mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Nicolas Capponi
06.12.2014 5673bc5e8c8e46c3764a67c74042fd9bc5ad8b92
Checkpoint commit for OPENDJ-1308 Migrate schema support

Preparation work to switch to new configuration framework (in opendj-config)
and new schema (in opendj-core).

This code is not used (called) yet by the server.
It contains several TODOs mainly to use correct messages for errors and logging.

* Add SchemaProvider interface, common interface to all schema providers
* Add CoreSchemaProvider class, providing core schema matching rules
and syntaxes
* Add SchemaUpdater interface, providing a way to abstract the
update of schema server
* Add SchemaProvider and CoreSchemaProvider xml configurations to
prevent errors when loading configuration with legacy config framework
* Add SchemaHandler class, responsible for loading the schema once
the configuration is available, using schema providers
* Add entry for Core Schema provider in config.ldif
* Add new objectclasses and attributes in 02-config.ldif schema file
for schema providers definitions

* Add dependency on opendj-config module to use the new configuration
framework classes
* Add ConfigurationHandler class, responsible to manage configuration entries
and listeners registration on these entries
** Will replace ConfigFileHandler
** Uses internally a MemoryBackend to manage entries
** Loads configuration using a config-enabled schema, thus avoiding
the double loading issue
* Add ConfigurationBootstrapper class

* Add ConfigurationBootstrapper class, update ServerContext and
DirectoryServer classes to enable construction of handlers
* Update DirectoryServer class with new initialization methods for
configuration and schema (not called yet when server is starting)
* Update DirectoryEnvironmentConfig class to allow usage when
there is a running server (as different instances of this class are used)

* Add unit test classes for SchemaHandler, ConfigurationHandler and
CoreSchemaProvider
* Add ServerContextBuilder class to ease construction of ServerContext
in tests
* Added a copy of ConfigurationMock class from opendj-config module
to use it in tests (temporary hack to avoid non obvious import of
tests jar from opendj-config module)
* Add resource/config-small.ldif file to have a reduced config file in some
tests
8 files modified
16 files added
3294 ■■■■■ changed files
opendj-sdk/opendj3-server-dev/ivy.xml 4 ●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/resource/config/config.ldif 15 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/resource/schema/02-config.ldif 46 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/admin/defn/org/opends/server/admin/std/CoreSchemaConfiguration.xml 187 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/admin/defn/org/opends/server/admin/std/RootConfiguration.xml 13 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/admin/defn/org/opends/server/admin/std/SchemaProviderConfiguration.xml 78 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/admin/messages/CoreSchemaCfgDefn.properties 18 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/admin/messages/RootCfgDefn.properties 4 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/admin/messages/SchemaProviderCfgDefn.properties 6 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/ConfigurationBootstrapper.java 72 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/ConfigurationHandler.java 957 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/DirectoryServer.java 76 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/SchemaHandler.java 373 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/ServerContext.java 11 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/schema/CoreSchemaProvider.java 156 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/schema/SchemaProvider.java 93 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/schema/SchemaUpdater.java 51 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/types/DirectoryEnvironmentConfig.java 114 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/resource/config-small.ldif 86 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/ConfigurationMock.java 246 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/ServerContextBuilder.java 103 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/core/ConfigurationHandlerTestCase.java 395 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/core/SchemaHandlerTestCase.java 96 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/schema/CoreSchemaProviderTestCase.java 94 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3-server-dev/ivy.xml
@@ -57,9 +57,7 @@
    <dependency org="org.forgerock.opendj"  name="opendj-server3x-adapter"  rev="&opendj.sdk.version;">
      <exclude module="opendj-server" />
    </dependency>
    <dependency org="org.forgerock.opendj"  name="opendj-server"            rev="&opendj.sdk.version;">
      <exclude module="opendj-config" />
    </dependency>
    <dependency org="org.forgerock.opendj"  name="opendj-server"            rev="&opendj.sdk.version;" />
    <dependency org="org.glassfish.grizzly" name="grizzly-http-servlet"     rev="&grizzly.version;">
      <exclude module="javax.servlet-api" />
    </dependency>
opendj-sdk/opendj3-server-dev/resource/config/config.ldif
@@ -69,6 +69,21 @@
ds-cfg-allowed-task: org.opends.server.tasks.ShutdownTask
ds-cfg-allowed-task: org.opends.server.tasks.PurgeConflictsHistoricalTask
dn: cn=Schema Providers,cn=config
objectClass: top
objectClass: ds-cfg-branch
cn: Schema Providers
dn: cn=Core Schema,cn=Schema Providers,cn=config
objectClass: top
objectClass: ds-cfg-schema-provider
objectClass: ds-cfg-core-schema
cn: Core Schema
ds-cfg-java-class: org.opends.server.schema.CoreSchemaProvider
ds-cfg-enabled: true
ds-cfg-strip-syntax-min-upper-bound-attribute-type-description: false
ds-cfg-strict-format-country-string: false
dn: cn=Access Control Handler,cn=config
objectClass: top
objectClass: ds-cfg-access-control-handler
opendj-sdk/opendj3-server-dev/resource/schema/02-config.ldif
@@ -3749,6 +3749,34 @@
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
  SINGLE-VALUE
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.136
  NAME 'ds-cfg-disabled-syntax'
  EQUALITY caseIgnoreMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.137
  NAME 'ds-cfg-disabled-matching-rule'
  EQUALITY caseIgnoreMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.138
  NAME 'ds-cfg-strict-format-country-string'
  EQUALITY booleanMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.139
  NAME 'ds-cfg-allow-zero-length-values-directory-string'
  EQUALITY booleanMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.140
  NAME 'ds-cfg-strip-syntax-min-upper-bound-attribute-type-description'
  EQUALITY booleanMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
  NAME 'ds-cfg-access-control-handler'
  SUP top
@@ -5684,3 +5712,21 @@
  STRUCTURAL
  MAY ds-cfg-pbkdf2-iterations
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.19
  NAME 'ds-cfg-schema-provider'
  SUP top
  STRUCTURAL
  MUST ( cn $
         ds-cfg-enabled $
         ds-cfg-java-class )
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.20
  NAME 'ds-cfg-core-schema'
  SUP ds-cfg-schema-provider
  STRUCTURAL
  MAY ( ds-cfg-disabled-syntax $
        ds-cfg-disabled-matching-rule $
        ds-cfg-strict-format-country-string $
        ds-cfg-allow-zero-length-values-directory-string $
        ds-cfg-strip-syntax-min-upper-bound-attribute-type-description )
  X-ORIGIN 'OpenDJ Directory Server' )
opendj-sdk/opendj3-server-dev/src/admin/defn/org/opends/server/admin/std/CoreSchemaConfiguration.xml
New file
@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
  ! CDDL HEADER START
  !
  ! The contents of this file are subject to the terms of the
  ! Common Development and Distribution License, Version 1.0 only
  ! (the "License").  You may not use this file except in compliance
  ! with the License.
  !
  ! You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
  ! or http://forgerock.org/license/CDDLv1.0.html.
  ! See the License for the specific language governing permissions
  ! and limitations under the License.
  !
  ! When distributing Covered Code, include this CDDL HEADER in each
  ! file and include the License file at legal-notices/CDDLv1_0.txt.
  ! If applicable, add the following below this CDDL HEADER, with the
  ! fields enclosed by brackets "[]" replaced with your own identifying
  ! information:
  !      Portions Copyright [yyyy] [name of copyright owner]
  !
  ! CDDL HEADER END
  !
  !
  !      Copyright 2014 ForgeRock AS.
  ! -->
<adm:managed-object name="core-schema" plural-name="core-schemas"
  package="org.opends.server.admin.std" extends="schema-provider"
  xmlns:adm="http://www.opends.org/admin"
  xmlns:ldap="http://www.opends.org/admin-ldap">
  <adm:synopsis>
    <adm:user-friendly-name />
    define the core schema elements to load.
  </adm:synopsis>
  <adm:description>
    Core schema provider configuration.
  </adm:description>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-core-schema</ldap:name>
      <ldap:superior>ds-cfg-schema-provider</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property-override name="java-class" advanced="true">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>
          org.opends.server.schema.CoreSchemaProvider
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
  <adm:property name="disabled-matching-rule" multi-valued="true">
   <adm:synopsis>
     The set of disabled matching rules.
   </adm:synopsis>
   <adm:description>
      Matching rules must be specified using the syntax: OID,
      or use the default value 'NONE' to specify no value.
   </adm:description>
   <adm:default-behavior>
     <adm:defined>
       <adm:value>NONE</adm:value>
     </adm:defined>
   </adm:default-behavior>
   <adm:syntax>
     <adm:string>
       <adm:pattern>
          <adm:regex>^([0-9.]+\\d|NONE)$</adm:regex>
          <adm:usage>OID</adm:usage>
          <adm:synopsis>
            The OID of the disabled matching rule.
          </adm:synopsis>
        </adm:pattern>
       </adm:string>
   </adm:syntax>
   <adm:profile name="ldap">
     <ldap:attribute>
       <ldap:name>ds-cfg-disabled-matching-rule</ldap:name>
     </ldap:attribute>
   </adm:profile>
 </adm:property>
 <adm:property name="disabled-syntax" multi-valued="true">
   <adm:synopsis>
     The set of disabled syntaxes.
   </adm:synopsis>
   <adm:description>
      Syntaxes must be specified using the syntax: OID,
      or use the default value 'NONE' to specify no value.
   </adm:description>
   <adm:default-behavior>
      <adm:defined>
        <adm:value>NONE</adm:value>
      </adm:defined>
   </adm:default-behavior>
   <adm:syntax>
     <adm:string>
       <adm:pattern>
          <adm:regex>^([0-9.]+\\d|NONE)$</adm:regex>
          <adm:usage>OID</adm:usage>
          <adm:synopsis>
            The OID of the disabled syntax, or NONE
          </adm:synopsis>
        </adm:pattern>
       </adm:string>
   </adm:syntax>
   <adm:profile name="ldap">
     <ldap:attribute>
       <ldap:name>ds-cfg-disabled-syntax</ldap:name>
     </ldap:attribute>
   </adm:profile>
 </adm:property>
 <adm:property name="strip-syntax-min-upper-bound-attribute-type-description" advanced="true">
    <adm:synopsis>
      Indicates whether the suggested minimum upper bound appended to an
      attribute's syntax OID in it's schema definition Attribute Type
      Description is stripped off.
    </adm:synopsis>
    <adm:description>
      When retrieving the server's schema, some APIs (JNDI) fail in
      their syntax lookup methods, because they do not parse this value
      correctly. This configuration option allows the server to be
      configured to provide schema definitions these APIs can parse
      correctly.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>false</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-strip-syntax-min-upper-bound-attribute-type-description</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="strict-format-country-string" advanced="true">
    <adm:synopsis>
      Indicates whether or not country code values are required to
      strictly comply with the standard definition for this syntax.
    </adm:synopsis>
    <adm:description>
      When set to false, country codes will not be validated and, as
      a result any string containing 2 characters will be acceptable.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>true</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-strict-format-country-string</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
    <adm:property name="allow-zero-length-values-directory-string" advanced="true">
    <adm:synopsis>
      Indicates whether zero-length (that is, an empty string) values are
      allowed for directory string.
    </adm:synopsis>
    <adm:description>
      This is technically not allowed by the revised LDAPv3
      specification, but some environments may require it for backward
      compatibility with servers that do allow it.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>false</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-allow-zero-length-values-directory-string</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opendj-sdk/opendj3-server-dev/src/admin/defn/org/opends/server/admin/std/RootConfiguration.xml
@@ -23,7 +23,7 @@
  !
  !
  !      Copyright 2007-2010 Sun Microsystems, Inc.
  !      Portions Copyright 2011 ForgeRock AS
  !      Portions Copyright 2011-2014 ForgeRock AS
  ! -->
<adm:root-managed-object xmlns:adm="http://www.opends.org/admin"
  xmlns:ldap="http://www.opends.org/admin-ldap"
@@ -40,6 +40,17 @@
      <ldap:rdn-sequence>cn=config</ldap:rdn-sequence>
    </adm:profile>
  </adm:relation>
  <adm:relation name="schema-provider">
    <adm:one-to-many />
    <adm:profile name="ldap">
      <ldap:rdn-sequence>cn=Schema Providers,cn=config</ldap:rdn-sequence>
    </adm:profile>
     <adm:profile name="cli">
      <cli:relation>
        <cli:default-property name="enabled" />
      </cli:relation>
    </adm:profile>
  </adm:relation>
  <adm:relation name="connection-handler">
    <adm:one-to-many />
    <adm:profile name="ldap">
opendj-sdk/opendj3-server-dev/src/admin/defn/org/opends/server/admin/std/SchemaProviderConfiguration.xml
New file
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
  ! CDDL HEADER START
  !
  ! The contents of this file are subject to the terms of the
  ! Common Development and Distribution License, Version 1.0 only
  ! (the "License").  You may not use this file except in compliance
  ! with the License.
  !
  ! You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
  ! or http://forgerock.org/license/CDDLv1.0.html.
  ! See the License for the specific language governing permissions
  ! and limitations under the License.
  !
  ! When distributing Covered Code, include this CDDL HEADER in each
  ! file and include the License file at legal-notices/CDDLv1_0.txt.
  ! If applicable, add the following below this CDDL HEADER, with the
  ! fields enclosed by brackets "[]" replaced with your own identifying
  ! information:
  !      Portions Copyright [yyyy] [name of copyright owner]
  !
  ! CDDL HEADER END
  !
  !
  !      Copyright 2014 ForgeRock AS.
  ! -->
<adm:managed-object name="schema-provider" plural-name="schema-providers"
  package="org.opends.server.admin.std"
  xmlns:adm="http://www.opends.org/admin"
  xmlns:ldap="http://www.opends.org/admin-ldap">
  <adm:synopsis>
    <adm:user-friendly-plural-name />
    define the schema elements to load.
  </adm:synopsis>
  <adm:description>
    Schema provider configuration.
  </adm:description>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-schema-provider</ldap:name>
      <ldap:superior>top</ldap:superior>
    </ldap:object-class>
  </adm:profile>
    <adm:property name="enabled" mandatory="true">
    <adm:synopsis>
      Indicates whether the
      <adm:user-friendly-name />
      is enabled for use.
    </adm:synopsis>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-enabled</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="java-class" mandatory="true">
    <adm:synopsis>
      Specifies the fully-qualified name of the Java class that provides the
      <adm:user-friendly-name />
      implementation.
    </adm:synopsis>
    <adm:syntax>
      <adm:java-class>
        <adm:instance-of>
          org.opends.server.schema.SchemaProvider
        </adm:instance-of>
      </adm:java-class>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-java-class</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opendj-sdk/opendj3-server-dev/src/admin/messages/CoreSchemaCfgDefn.properties
New file
@@ -0,0 +1,18 @@
user-friendly-name=Core Schema
user-friendly-plural-name=Core Schemas
synopsis=Core Schema define the core schema elements to load.
description=Core schema provider configuration.
property.allow-zero-length-values-directory-string.synopsis=Indicates whether zero-length (that is, an empty string) values are allowed for directory string.
property.allow-zero-length-values-directory-string.description=This is technically not allowed by the revised LDAPv3 specification, but some environments may require it for backward compatibility with servers that do allow it.
property.disabled-matching-rule.synopsis=The set of disabled matching rules.
property.disabled-matching-rule.description=Matching rules must be specified using the syntax: OID, or use the default value 'NONE' to specify no value.
property.disabled-matching-rule.syntax.string.pattern.synopsis=The OID of the disabled matching rule.
property.disabled-syntax.synopsis=The set of disabled syntaxes.
property.disabled-syntax.description=Syntaxes must be specified using the syntax: OID, or use the default value 'NONE' to specify no value.
property.disabled-syntax.syntax.string.pattern.synopsis=The OID of the disabled syntax, or NONE
property.enabled.synopsis=Indicates whether the Core Schema is enabled for use.
property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the Core Schema implementation.
property.strict-format-country-string.synopsis=Indicates whether or not country code values are required to strictly comply with the standard definition for this syntax.
property.strict-format-country-string.description=When set to false, country codes will not be validated and, as a result any string containing 2 characters will be acceptable.
property.strip-syntax-min-upper-bound-attribute-type-description.synopsis=Indicates whether the suggested minimum upper bound appended to an attribute's syntax OID in it's schema definition Attribute Type Description is stripped off.
property.strip-syntax-min-upper-bound-attribute-type-description.description=When retrieving the server's schema, some APIs (JNDI) fail in their syntax lookup methods, because they do not parse this value correctly. This configuration option allows the server to be configured to provide schema definitions these APIs can parse correctly.
opendj-sdk/opendj3-server-dev/src/admin/messages/RootCfgDefn.properties
@@ -104,6 +104,10 @@
relation.sasl-mechanism-handler.user-friendly-plural-name=SASL Mechanism Handlers
relation.sasl-mechanism-handler.synopsis=The SASL mechanism handler configuration entry is the parent for all SASL mechanism handlers defined in the OpenDJ directory server.
relation.sasl-mechanism-handler.description=SASL mechanism handlers are responsible for authenticating users during the course of processing a SASL (Simple Authentication and Security Layer, as defined in RFC 4422) bind.
relation.schema-provider.user-friendly-name=Schema Provider
relation.schema-provider.user-friendly-plural-name=Schema Providers
relation.schema-provider.synopsis=Schema Providers define the schema elements to load.
relation.schema-provider.description=Schema provider configuration.
relation.synchronization-provider.user-friendly-name=Synchronization Provider
relation.synchronization-provider.user-friendly-plural-name=Synchronization Providers
relation.synchronization-provider.synopsis=Synchronization Providers are responsible for handling synchronization of the directory server data with other OpenDJ instances or other data repositories.
opendj-sdk/opendj3-server-dev/src/admin/messages/SchemaProviderCfgDefn.properties
New file
@@ -0,0 +1,6 @@
user-friendly-name=Schema Provider
user-friendly-plural-name=Schema Providers
synopsis=Schema Providers define the schema elements to load.
description=Schema provider configuration.
property.enabled.synopsis=Indicates whether the Schema Provider is enabled for use.
property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the Schema Provider implementation.
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/ConfigurationBootstrapper.java
New file
@@ -0,0 +1,72 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS.
 */
package org.opends.server.core;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.ConfigurationFramework;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ServerManagementContext;
import org.opends.server.types.InitializationException;
/**
 * Bootstrap the server configuration.
 */
public class ConfigurationBootstrapper
{
  /**
   * Bootstrap the server configuration.
   * <p>
   * The returned server management context is fully initialized with
   * all configuration objects valued from configuration file.
   *
   * @param serverContext
   *            The server context.
   * @return the server management context
   * @throws InitializationException
   *            If an error occurs during bootstrapping.
   */
  public static ServerManagementContext bootstrap(ServerContext serverContext) throws InitializationException {
    final ConfigurationFramework configFramework = ConfigurationFramework.getInstance();
    try
    {
      if (!configFramework.isInitialized())
      {
        configFramework.initialize();
      }
    }
    catch (ConfigException e)
    {
      // TODO : fix the message
      throw new InitializationException(LocalizableMessage.raw("Cannot initialize config framework"), e);
    }
    final ConfigurationHandler configurationHandler = new ConfigurationHandler(serverContext);
    configurationHandler.initialize();
    return new ServerManagementContext(configurationHandler);
  }
}
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/ConfigurationHandler.java
New file
@@ -0,0 +1,957 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS
 */
package org.opends.server.core;
import static org.forgerock.util.Utils.*;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.adapter.server3x.Converters;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.spi.ConfigAddListener;
import org.forgerock.opendj.config.server.spi.ConfigChangeListener;
import org.forgerock.opendj.config.server.spi.ConfigDeleteListener;
import org.forgerock.opendj.config.server.spi.ConfigurationRepository;
import org.forgerock.opendj.ldap.CancelRequestListener;
import org.forgerock.opendj.ldap.CancelledResultException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldap.RequestContext;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.ResultHandler;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
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.ldap.responses.SearchResultReference;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.opendj.ldif.EntryReader;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.util.Utils;
import org.opends.server.types.DirectoryEnvironmentConfig;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
/**
 * Responsible for managing configuration entries and listeners on these
 * entries.
 */
public class ConfigurationHandler implements ConfigurationRepository
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  private static final String CONFIGURATION_FILE_NAME = "02-config.ldif";
  private final ServerContext serverContext;
  /** The complete path to the configuration file to use. */
  private File configFile;
  /** Indicates whether to start using the last known good configuration. */
  private boolean useLastKnownGoodConfig;
  /** Backend containing the configuration entries. */
  private MemoryBackend backend;
  /** The config root entry. */
  private Entry rootEntry;
  /** The add/delete/change listeners on configuration entries. */
  private final ConcurrentHashMap<DN, EntryListeners> listeners = new ConcurrentHashMap<DN, EntryListeners>();
  /** Schema with configuration-related elements. */
  private Schema configEnabledSchema;
  /**
   * Creates a new instance.
   *
   * @param serverContext
   *          The server context.
   */
  public ConfigurationHandler(final ServerContext serverContext)
  {
    this.serverContext = serverContext;
  }
  /**
   * Initialize the configuration.
   *
   * @throws InitializationException
   *            If an error occurs during the initialization.
   */
  public void initialize() throws InitializationException
  {
    final DirectoryEnvironmentConfig environment = serverContext.getEnvironment();
    useLastKnownGoodConfig = environment.useLastKnownGoodConfiguration();
    configFile = findConfigFileToUse(environment.getConfigFile());
    configEnabledSchema = loadConfigEnabledSchema();
    loadConfiguration(configFile, configEnabledSchema);
  }
  /** Holds add, change and delete listeners for a given configuration entry. */
  private static class EntryListeners {
    /** The set of add listeners that have been registered with this entry. */
    private final CopyOnWriteArrayList<ConfigAddListener> addListeners =
        new CopyOnWriteArrayList<ConfigAddListener>();
    /** The set of change listeners that have been registered with this entry. */
    private final CopyOnWriteArrayList<ConfigChangeListener> changeListeners =
        new CopyOnWriteArrayList<ConfigChangeListener>();
    /** The set of delete listeners that have been registered with this entry. */
    private final CopyOnWriteArrayList<ConfigDeleteListener> deleteListeners =
        new CopyOnWriteArrayList<ConfigDeleteListener>();
    CopyOnWriteArrayList<ConfigChangeListener> getChangeListeners()
    {
      return changeListeners;
    }
    void registerChangeListener(final ConfigChangeListener listener)
    {
      changeListeners.add(listener);
    }
    boolean deregisterChangeListener(final ConfigChangeListener listener)
    {
      return changeListeners.remove(listener);
    }
    CopyOnWriteArrayList<ConfigAddListener> getAddListeners()
    {
      return addListeners;
    }
    void registerAddListener(final ConfigAddListener listener)
    {
      addListeners.addIfAbsent(listener);
    }
    void deregisterAddListener(final ConfigAddListener listener)
    {
      addListeners.remove(listener);
    }
    CopyOnWriteArrayList<ConfigDeleteListener> getDeleteListeners()
    {
      return deleteListeners;
    }
    void registerDeleteListener(final ConfigDeleteListener listener)
    {
      deleteListeners.addIfAbsent(listener);
    }
    void deregisterDeleteListener(final ConfigDeleteListener listener)
    {
      deleteListeners.remove(listener);
    }
  }
  /** Request context to be used when requesting the internal backend. */
  private static final RequestContext UNCANCELLABLE_REQUEST_CONTEXT =
      new RequestContext()
      {
        /** {@inheritDoc} */        @Override
        public void removeCancelRequestListener(final CancelRequestListener listener)
        {
          // nothing to do
        }
        /** {@inheritDoc} */
        @Override
        public int getMessageID()
        {
          return -1;
        }
        /** {@inheritDoc} */
        @Override
        public void checkIfCancelled(final boolean signalTooLate)
            throws CancelledResultException
        {
          // nothing to do
        }
        /** {@inheritDoc} */
        @Override
        public void addCancelRequestListener(final CancelRequestListener listener)
        {
          // nothing to do
        }
      };
  /** Handler for search results.  */
  private static final class ConfigSearchResultHandler implements SearchResultHandler
  {
    private ErrorResultException resultError;
    private final Set<Entry> entries = new HashSet<Entry>();
    ErrorResultException getResultError()
    {
      return resultError;
    }
    boolean hasCompletedSuccessfully() {
      return resultError == null;
    }
    Set<Entry> getEntries()
    {
      return entries;
    }
    /** {@inheritDoc} */
    @Override
    public void handleResult(Result result)
    {
      // nothing to do
    }
    /** {@inheritDoc} */
    @Override
    public void handleErrorResult(ErrorResultException error)
    {
      resultError = error;
    }
    /** {@inheritDoc} */
    @Override
    public boolean handleReference(SearchResultReference reference)
    {
      throw new UnsupportedOperationException("Search references are not supported for configuration entries.");
    }
    /** {@inheritDoc} */
    @Override
    public boolean handleEntry(SearchResultEntry entry)
    {
      entries.add(entry);
      return true;
    }
  }
  /** Handler for LDAP operations. */
  private static final class ConfigResultHandler implements ResultHandler<Result> {
    private ErrorResultException resultError;
    ErrorResultException getResultError()
    {
      return resultError;
    }
    boolean hasCompletedSuccessfully() {
      return resultError == null;
    }
    /** {@inheritDoc} */
    @Override
    public void handleResult(Result result)
    {
      // nothing to do
    }
    /** {@inheritDoc} */
    @Override
    public void handleErrorResult(ErrorResultException error)
    {
      resultError = error;
    }
  }
  /**
   * Returns the configuration root entry.
   *
   * @return the root entry
   */
  public Entry getRootEntry() {
    return rootEntry;
  }
  /** {@inheritDoc} */
  /** {@inheritDoc} */  @Override
  public Entry getEntry(final DN dn) throws ConfigException {
    Entry entry = backend.get(dn);
    if (entry == null)
    {
      // TODO : fix message
      LocalizableMessage message = LocalizableMessage.raw("Unable to retrieve the configuration entry %s", dn);
      throw new ConfigException(message);
    }
    return entry;
  }
  /** {@inheritDoc} */
  /** {@inheritDoc} */  @Override
  public boolean hasEntry(final DN dn) throws ConfigException {
    return backend.get(dn) != null;
  }
  /** {@inheritDoc} */
  /** {@inheritDoc} */  @Override
  public Set<DN> getChildren(DN dn) throws ConfigException {
    final ConfigSearchResultHandler resultHandler = new ConfigSearchResultHandler();
    backend.handleSearch(
        UNCANCELLABLE_REQUEST_CONTEXT,
        Requests.newSearchRequest(dn, SearchScope.SINGLE_LEVEL, Filter.objectClassPresent()),
        null,
        resultHandler);
    if (resultHandler.hasCompletedSuccessfully())
    {
      final Set<DN> children = new HashSet<DN>();
      for (final Entry entry : resultHandler.getEntries())
      {
        children.add(entry.getName());
      }
      return children;
    }
    else {
      // TODO : fix message
      throw new ConfigException(
          LocalizableMessage.raw("Unable to retrieve children of configuration entry : %s", dn),
          resultHandler.getResultError());
    }
  }
  /**
   * Retrieves the number of subordinates for the requested entry.
   *
   * @param entryDN
   *          The distinguished name of the entry.
   * @param subtree
   *          {@code true} to include all entries from the requested entry
   *          to the lowest level in the tree or {@code false} to only
   *          include the entries immediately below the requested entry.
   * @return The number of subordinate entries
   * @throws ConfigException
   *           If a problem occurs while trying to retrieve the entry.
   */
  public long numSubordinates(final DN entryDN, final boolean subtree) throws ConfigException
  {
    final ConfigSearchResultHandler resultHandler = new ConfigSearchResultHandler();
    final SearchScope scope = subtree ? SearchScope.SUBORDINATES : SearchScope.SINGLE_LEVEL;
    backend.handleSearch(
        UNCANCELLABLE_REQUEST_CONTEXT,
        Requests.newSearchRequest(entryDN, scope, Filter.objectClassPresent()),
        null,
        resultHandler);
    if (resultHandler.hasCompletedSuccessfully())
    {
      return resultHandler.getEntries().size();
    }
    else {
      // TODO : fix the message
      throw new ConfigException(
          LocalizableMessage.raw("Unable to retrieve children of configuration entry : %s", entryDN),
          resultHandler.getResultError());
    }
  }
  /**
   * Add a configuration entry
   * <p>
   * The add is performed only if all Add listeners on the parent entry accept
   * the changes. Once the change is accepted, entry is effectively added and
   * all Add listeners are called again to apply the change resulting from this
   * new entry.
   *
   * @param entry
   *          The configuration entry to add.
   * @throws DirectoryException
   *           If an error occurs.
   */
  public void addEntry(final Entry entry) throws DirectoryException
  {
    final DN entryDN = entry.getName();
    if (backend.contains(entryDN))
    {
      throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, ERR_CONFIG_FILE_ADD_ALREADY_EXISTS.get(entryDN));
    }
    final DN parentDN = retrieveParentDN(entryDN);
    // Iterate through add listeners to make sure the new entry is acceptable.
    final List<ConfigAddListener> addListeners = getAddListeners(parentDN);
    final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
    for (final ConfigAddListener listener : addListeners)
    {
      if (!listener.configAddIsAcceptable(entry, unacceptableReason))
      {
        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
            ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entryDN, parentDN, unacceptableReason));
      }
    }
    // Add the entry.
    final ConfigResultHandler resultHandler = new ConfigResultHandler();
    backend.handleAdd(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newAddRequest(entry), null, resultHandler);
    if (!resultHandler.hasCompletedSuccessfully()) {
      // TODO fix the message : error when adding config entry
      // use resultHandler.getResultError() to get the error
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entryDN, parentDN, unacceptableReason));
    }
    // Notify all the add listeners to apply the new configuration entry.
    ResultCode resultCode = ResultCode.SUCCESS;
    final List<LocalizableMessage> messages = new LinkedList<LocalizableMessage>();
    for (final ConfigAddListener listener : addListeners)
    {
      final ConfigChangeResult result = listener.applyConfigurationAdd(entry);
      if (result.getResultCode() != ResultCode.SUCCESS)
      {
        resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode;
        messages.addAll(result.getMessages());
      }
      handleConfigChangeResult(result, entry.getName(), listener.getClass().getName(), "applyConfigurationAdd");
    }
    if (resultCode != ResultCode.SUCCESS)
    {
      final String reasons = Utils.joinAsString(".  ", messages);
      throw new DirectoryException(resultCode, ERR_CONFIG_FILE_ADD_APPLY_FAILED.get(reasons));
    }
  }
  /**
   * Delete a configuration entry.
   * <p>
   * The delete is performed only if all Delete listeners on the parent entry
   * accept the changes. Once the change is accepted, entry is effectively
   * deleted and all Delete listeners are called again to apply the change
   * resulting from this deletion.
   *
   * @param dn
   *          DN of entry to delete.
   * @throws DirectoryException
   *           If a problem occurs.
   */
  public void deleteEntry(final DN dn) throws DirectoryException
  {
    // Entry must exist.
    if (!backend.contains(dn))
    {
      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
          ERR_CONFIG_FILE_DELETE_NO_SUCH_ENTRY.get(dn), Converters.to(getMatchedDN(dn)), null);
    }
    // Entry must not have children.
    try
    {
      if (!getChildren(dn).isEmpty())
      {
        throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF,
            ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn));
      }
    }
    catch (ConfigException e)
    {
      // TODO : fix message = ERROR BACKEND CONFIG
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn), e);
    }
    // TODO : pass in the localizable message (2)
    final DN parentDN = retrieveParentDN(dn);
    // Iterate through delete listeners to make sure the deletion is acceptable.
    final List<ConfigDeleteListener> deleteListeners = getDeleteListeners(parentDN);
    final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
    final Entry entry = backend.get(dn);
    for (final ConfigDeleteListener listener : deleteListeners)
    {
      if (!listener.configDeleteIsAcceptable(entry, unacceptableReason))
      {
        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
            ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entry, parentDN, unacceptableReason));
      }
    }
    // Delete the entry
    final ConfigResultHandler resultHandler = new ConfigResultHandler();
    backend.handleDelete(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newDeleteRequest(dn), null, resultHandler);
    if (!resultHandler.hasCompletedSuccessfully()) {
      // TODO fix message : error when deleting config entry
      // use resultHandler.getResultError() to get the error
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_CONFIG_FILE_DELETE_REJECTED.get(dn, parentDN, unacceptableReason));
    }
    // Notify all the delete listeners that the entry has been removed.
    ResultCode resultCode = ResultCode.SUCCESS;
    final List<LocalizableMessage> messages = new LinkedList<LocalizableMessage>();
    for (final ConfigDeleteListener listener : deleteListeners)
    {
      final ConfigChangeResult result = listener.applyConfigurationDelete(entry);
      if (result.getResultCode() != ResultCode.SUCCESS)
      {
        resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode;
        messages.addAll(result.getMessages());
      }
      handleConfigChangeResult(result, dn, listener.getClass().getName(), "applyConfigurationDelete");
    }
    if (resultCode != ResultCode.SUCCESS)
    {
      final String reasons = Utils.joinAsString(".  ", messages);
      throw new DirectoryException(resultCode, ERR_CONFIG_FILE_DELETE_APPLY_FAILED.get(reasons));
    }
  }
  /**
   * Replaces the old configuration entry with the new configuration entry
   * provided.
   * <p>
   * The replacement is performed only if all Change listeners on the entry
   * accept the changes. Once the change is accepted, entry is effectively
   * replaced and all Change listeners are called again to apply the change
   * resulting from the replacement.
   *
   * @param oldEntry
   *          The original entry that is being replaced.
   * @param newEntry
   *          The new entry to use in place of the existing entry with the same
   *          DN.
   * @throws DirectoryException
   *           If a problem occurs while trying to replace the entry.
   */
  public void replaceEntry(final Entry oldEntry, final Entry newEntry)
      throws DirectoryException
  {
    final DN entryDN = oldEntry.getName();
    if (!backend.contains(entryDN))
    {
      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
          ERR_CONFIG_FILE_MODIFY_NO_SUCH_ENTRY.get(oldEntry), Converters.to(getMatchedDN(entryDN)), null);
    }
    //TODO : add objectclass and attribute to the config schema in order to get this code run
//    if (!Entries.getStructuralObjectClass(oldEntry, configEnabledSchema)
//        .equals(Entries.getStructuralObjectClass(newEntry, configEnabledSchema)))
//    {
//      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
//          ERR_CONFIG_FILE_MODIFY_STRUCTURAL_CHANGE_NOT_ALLOWED.get(entryDN));
//    }
    // Iterate through change listeners to make sure the change is acceptable.
    final List<ConfigChangeListener> changeListeners = getChangeListeners(entryDN);
    final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
    for (ConfigChangeListener listeners : changeListeners)
    {
      if (!listeners.configChangeIsAcceptable(newEntry, unacceptableReason))
      {
        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
            ERR_CONFIG_FILE_MODIFY_REJECTED_BY_CHANGE_LISTENER.get(entryDN, unacceptableReason));
      }
    }
    // Replace the old entry with new entry.
    final ConfigResultHandler resultHandler = new ConfigResultHandler();
    backend.handleModify(
        UNCANCELLABLE_REQUEST_CONTEXT,
        Requests.newModifyRequest(oldEntry, newEntry),
        null,
        resultHandler);
    if (!resultHandler.hasCompletedSuccessfully())
    {
      // TODO fix message : error when replacing config entry
      // use resultHandler.getResultError() to get the error
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_CONFIG_FILE_DELETE_REJECTED.get(entryDN, entryDN, unacceptableReason));
    }
    // Notify all the change listeners of the update.
    ResultCode resultCode = ResultCode.SUCCESS;
    final List<LocalizableMessage> messages = new LinkedList<LocalizableMessage>();
    for (final ConfigChangeListener listener : changeListeners)
    {
      final ConfigChangeResult result = listener.applyConfigurationChange(newEntry);
      if (result.getResultCode() != ResultCode.SUCCESS)
      {
        resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode;
        messages.addAll(result.getMessages());
      }
      handleConfigChangeResult(result, entryDN, listener.getClass().getName(), "applyConfigurationChange");
    }
    if (resultCode != ResultCode.SUCCESS)
    {
      throw new DirectoryException(resultCode,
          ERR_CONFIG_FILE_MODIFY_APPLY_FAILED.get(Utils.joinAsString(".  ", messages)));
    }
  }
  /** {@inheritDoc} */
  @Override
  public void registerAddListener(final DN dn, final ConfigAddListener listener)
  {
    getEntryListeners(dn).registerAddListener(listener);
  }
  /** {@inheritDoc} */
  @Override
  public void registerDeleteListener(final DN dn, final ConfigDeleteListener listener)
  {
    getEntryListeners(dn).registerDeleteListener(listener);
  }
  /** {@inheritDoc} */
  @Override
  public void registerChangeListener(final DN dn, final ConfigChangeListener listener)
  {
    getEntryListeners(dn).registerChangeListener(listener);
  }
  /** {@inheritDoc} */
  @Override
  public void deregisterAddListener(final DN dn, final ConfigAddListener listener)
  {
    getEntryListeners(dn).deregisterAddListener(listener);
  }
  /** {@inheritDoc} */
  @Override
  public void deregisterDeleteListener(final DN dn, final ConfigDeleteListener listener)
  {
    getEntryListeners(dn).deregisterDeleteListener(listener);
  }
  /** {@inheritDoc} */
  @Override
  public boolean deregisterChangeListener(final DN dn, final ConfigChangeListener listener)
  {
    return getEntryListeners(dn).deregisterChangeListener(listener);
  }
  /** {@inheritDoc} */
  @Override
  public List<ConfigAddListener> getAddListeners(final DN dn)
  {
    return getEntryListeners(dn).getAddListeners();
  }
  /** {@inheritDoc} */
  @Override
  public List<ConfigDeleteListener> getDeleteListeners(final DN dn)
  {
    return getEntryListeners(dn).getDeleteListeners();
  }
  /** {@inheritDoc} */
  @Override
  public List<ConfigChangeListener> getChangeListeners(final DN dn)
  {
    return getEntryListeners(dn).getChangeListeners();
  }
  /** Load the configuration-enabled schema that will allow to read configuration file. */
  private Schema loadConfigEnabledSchema() throws InitializationException {
    LDIFEntryReader reader = null;
    try
    {
      final File schemaDir = serverContext.getEnvironment().getSchemaDirectory();
      reader = new LDIFEntryReader(new FileReader(new File(schemaDir, CONFIGURATION_FILE_NAME)));
      reader.setSchema(Schema.getDefaultSchema());
      final Entry entry = reader.readEntry();
      return new SchemaBuilder(Schema.getDefaultSchema()).addSchema(entry, false).toSchema();
    }
    catch (Exception e)
    {
      // TODO : fix message
      throw new InitializationException(LocalizableMessage.raw("Unable to load config-enabled schema"), e);
    }
    finally {
      closeSilently(reader);
    }
  }
  /**
   * Read configuration entries from provided configuration file.
   *
   * @param configFile
   *            LDIF file with configuration entries.
   * @param schema
   *          Schema to validate entries when reading the config file.
   * @throws InitializationException
   *            If an errors occurs.
   */
  private void loadConfiguration(final File configFile, final Schema schema)
      throws InitializationException
  {
    EntryReader reader = null;
    try
    {
      reader = getLDIFReader(configFile, schema);
      backend = new MemoryBackend(schema, reader);
    }
    catch (IOException e)
    {
      throw new InitializationException(
          ERR_CONFIG_FILE_GENERIC_ERROR.get(configFile.getAbsolutePath(), e.getCause()), e);
    }
    finally
    {
      closeSilently(reader);
    }
    // Check that root entry is the expected one
    rootEntry = backend.get(DN_CONFIG_ROOT);
    if (rootEntry == null)
    {
      // fix message : we didn't find the expected root in the file
      throw new InitializationException(ERR_CONFIG_FILE_INVALID_BASE_DN.get(
          configFile.getAbsolutePath(), "", DN_CONFIG_ROOT));
    }
  }
  /**
   * Returns the LDIF reader on configuration entries.
   * <p>
   * It is the responsability of the caller to ensure that reader
   * is closed after usage.
   *
   * @param configFile
   *          LDIF file containing the configuration entries.
   * @param schema
   *          Schema to validate entries when reading the config file.
   * @return the LDIF reader
   * @throws InitializationException
   *           If an error occurs.
   */
  private EntryReader getLDIFReader(final File configFile, final Schema schema)
      throws InitializationException
  {
    LDIFEntryReader reader = null;
    try
    {
      reader = new LDIFEntryReader(new FileReader(configFile));
      reader.setSchema(schema);
    }
    catch (Exception e)
    {
      throw new InitializationException(
          ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(configFile.getAbsolutePath(), e), e);
    }
    return reader;
  }
  /**
   * Returns the entry listeners attached to the provided DN.
   * <p>
   * If no listener exist for the provided DN, then a new set of empty listeners
   * is created and returned.
   *
   * @param dn
   *          DN of a configuration entry.
   * @return the listeners attached to the corresponding configuration entry.
   */
  private EntryListeners getEntryListeners(final DN dn) {
    EntryListeners entryListeners  = listeners.get(dn);
    if (entryListeners == null) {
      entryListeners = new EntryListeners();
      final EntryListeners previousListeners = listeners.putIfAbsent(dn, entryListeners);
      if (previousListeners != null) {
        entryListeners = previousListeners;
      }
    }
    return entryListeners;
  }
  /**
   * Returns the parent DN of the configuration entry corresponding to the
   * provided DN.
   *
   * @param entryDN
   *          DN of entry to retrieve the parent from.
   * @return the parent DN
   * @throws DirectoryException
   *           If entry has no parent or parent entry does not exist.
   */
  private DN retrieveParentDN(final DN entryDN) throws DirectoryException
  {
    final DN parentDN = entryDN.parent();
    // Entry must have a parent.
    if (parentDN == null)
    {
      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_ADD_NO_PARENT_DN.get(entryDN));
    }
    // Parent entry must exist.
    if (!backend.contains(parentDN))
    {
      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
          ERR_CONFIG_FILE_ADD_NO_PARENT.get(entryDN, parentDN), Converters.to(getMatchedDN(parentDN)), null);
    }
    return parentDN;
  }
  /**
   * Returns the matched DN that is available in the configuration for the
   * provided DN.
   */
  private DN getMatchedDN(final DN dn)
  {
    DN matchedDN = null;
    DN parentDN = dn.parent();
    while (parentDN != null)
    {
      if (backend.contains(parentDN))
      {
        matchedDN = parentDN;
        break;
      }
      parentDN = parentDN.parent();
    }
    return matchedDN;
  }
  /**
   * Find the actual configuration file to use to load configuration, given the
   * standard config file.
   *
   * @param standardConfigFile
   *          "Standard" configuration file provided.
   * @return the actual configuration file to use, which is either the standard
   *         config file provided or the config file corresponding to the last
   *         known good configuration
   * @throws InitializationException
   *           If a problem occurs.
   */
  private File findConfigFileToUse(final File standardConfigFile) throws InitializationException
  {
    File configFileToUse = null;
    if (useLastKnownGoodConfig)
    {
      configFileToUse = new File(standardConfigFile + ".startok");
      if (! configFileToUse.exists())
      {
        logger.warn(WARN_CONFIG_FILE_NO_STARTOK_FILE, configFileToUse.getAbsolutePath(), standardConfigFile);
        useLastKnownGoodConfig = false;
        configFileToUse = standardConfigFile;
      }
      else
      {
        logger.info(NOTE_CONFIG_FILE_USING_STARTOK_FILE, configFileToUse.getAbsolutePath(), standardConfigFile);
      }
    }
    else
    {
      configFileToUse = standardConfigFile;
    }
    try
    {
      if (! configFileToUse.exists())
      {
        throw new InitializationException(ERR_CONFIG_FILE_DOES_NOT_EXIST.get(configFileToUse.getAbsolutePath()));
      }
    }
    catch (Exception e)
    {
      throw new InitializationException(
          ERR_CONFIG_FILE_CANNOT_VERIFY_EXISTENCE.get(configFileToUse.getAbsolutePath(), e));
    }
    return configFileToUse;
  }
  /**
   * Examines the provided result and logs a message if appropriate. If the
   * result code is anything other than {@code SUCCESS}, then it will log an
   * error message. If the operation was successful but admin action is
   * required, then it will log a warning message. If no action is required but
   * messages were generated, then it will log an informational message.
   *
   * @param result
   *          The config change result object that
   * @param entryDN
   *          The DN of the entry that was added, deleted, or modified.
   * @param className
   *          The name of the class for the object that generated the provided
   *          result.
   * @param methodName
   *          The name of the method that generated the provided result.
   */
  private void handleConfigChangeResult(ConfigChangeResult result, DN entryDN, String className, String methodName)
  {
    if (result == null)
    {
      logger.error(ERR_CONFIG_CHANGE_NO_RESULT, className, methodName, entryDN);
      return;
    }
    final ResultCode resultCode = result.getResultCode();
    final boolean adminActionRequired = result.adminActionRequired();
    final List<LocalizableMessage> messages = result.getMessages();
    final String messageBuffer = Utils.joinAsString("  ", messages);
    if (resultCode != ResultCode.SUCCESS)
    {
      logger.error(ERR_CONFIG_CHANGE_RESULT_ERROR, className, methodName, entryDN, resultCode,
          adminActionRequired, messageBuffer);
    }
    else if (adminActionRequired)
    {
      logger.warn(WARN_CONFIG_CHANGE_RESULT_ACTION_REQUIRED, className, methodName, entryDN, messageBuffer);
    }
    else if (messageBuffer.length() > 0)
    {
      logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messageBuffer);
    }
  }
}
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/DirectoryServer.java
@@ -45,6 +45,7 @@
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.schema.AttributeUsage;
import org.forgerock.opendj.ldap.schema.ObjectClassType;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.util.Reject;
import org.forgerock.util.Utils;
import org.opends.server.admin.AdministrationConnector;
@@ -628,6 +629,10 @@
  /** The schema for the Directory Server. */
  private Schema schema;
  /** The schema for the Directory Server. */
  // TODO : temporary field to be removed once old schema is completely removed
  private org.forgerock.opendj.ldap.schema.Schema schemaNG;
  /** The schema configuration manager for the Directory Server. */
  private SchemaConfigManager schemaConfigManager;
@@ -727,6 +732,9 @@
  /** Temporary context object, to provide instance methods instead of static methods. */
  private final DirectoryServerContext serverContext = new DirectoryServerContext();
  /** Entry point for server configuration. */
  private org.forgerock.opendj.config.server.ServerManagementContext serverManagementContext;
  /**
   * Creates a new instance of the Directory Server.  This will allow only a
   * single instance of the server per JVM.
@@ -775,9 +783,30 @@
    /** {@inheritDoc} */
    @Override
    public ServerManagementContext getServerManagementContext()
    public SchemaUpdater getSchemaUpdater()
    {
      return ServerManagementContext.getInstance();
      return new SchemaUpdater()
      {
        @Override
        public boolean updateSchema(SchemaBuilder schemaBuilder)
        {
          schemaNG = schemaBuilder.toSchema();
          return true;
        }
        @Override
        public SchemaBuilder getSchemaBuilder()
        {
          return new SchemaBuilder(schemaNG);
        }
      };
    }
    /** {@inheritDoc} */
    @Override
    public org.forgerock.opendj.config.server.ServerManagementContext getServerManagementContext()
    {
      return serverManagementContext;
    }
  }
@@ -1142,7 +1171,42 @@
    initializeConfiguration();
  }
  /**
   * Initialize this server.
   * <p>
   * Initialization involves the following steps:
   * <ul>
   *  <li>Configuration</li>
   *  <li>Schema</li>
   * </ul>
   * @throws InitializationException
   */
  private void initializeNG() throws InitializationException
  {
    serverManagementContext = ConfigurationBootstrapper.bootstrap(serverContext);
    initializeSchemaNG();
    // TODO : config backend should be initialized later, with the other backends
    //ConfigBackend configBackend = new ConfigBackend();
    //configBackend.initializeConfigBackend(serverContext, configurationHandler);
  }
  /**
   * Initialize the schema of this server.
   */
  private void initializeSchemaNG() throws InitializationException
  {
    SchemaHandler schemaHandler = new SchemaHandler();
    try
    {
      schemaHandler.initialize(serverContext);
    }
    catch (org.forgerock.opendj.config.server.ConfigException e)
    {
      // TODO : fix message
      throw new InitializationException(LocalizableMessage.raw("Cannot initialize schema handler"), e);
    }
  }
  /**
   * Instantiates the configuration handler and loads the Directory Server
@@ -1157,7 +1221,6 @@
    this.configClass = environmentConfig.getConfigClass();
    this.configFile  = environmentConfig.getConfigFile();
    // Make sure that administration framework definition classes are loaded.
    ClassLoaderProvider provider = ClassLoaderProvider.getInstance();
    if (! provider.isEnabled())
@@ -1165,7 +1228,6 @@
      provider.enable();
    }
    // Load and instantiate the configuration handler class.
    Class<ConfigHandler> handlerClass = configClass;
    try
@@ -1182,6 +1244,8 @@
    }
    // Perform the handler-specific initialization.
    try
    {
@@ -1315,7 +1379,6 @@
      // Initialize all the schema elements.
      initializeSchema();
      // Initialize the plugin manager so that internal plugins can be
      // registered.
      pluginConfigManager.initializePluginConfigManager();
@@ -9055,9 +9118,6 @@
    PrintStream serverOutStream;
    try
    {
      // We need to figure out where to put the file.  See if the server root
      // is available as an environment variable and if so then use it.
      // Otherwise, try to figure it out from the location of the config file.
      File serverRoot = environmentConfig.getServerRoot();
      if (serverRoot == null)
      {
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/SchemaHandler.java
New file
@@ -0,0 +1,373 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS
 */
package org.opends.server.core;
import static org.forgerock.util.Utils.*;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.util.StaticUtils.*;
import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.ClassPropertyDefinition;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.opendj.ldif.EntryReader;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.opendj.server.config.meta.SchemaProviderCfgDefn;
import org.forgerock.opendj.server.config.server.RootCfg;
import org.forgerock.opendj.server.config.server.SchemaProviderCfg;
import org.forgerock.util.Utils;
import org.opends.server.schema.SchemaProvider;
import org.opends.server.schema.SchemaUpdater;
import org.opends.server.types.InitializationException;
/**
 * Responsible for loading the server schema.
 * <p>
 * The schema is loaded in three steps :
 * <ul>
 *   <li>Start from the core schema.</li>
 *   <li>Load schema elements from the schema providers defined in configuration.</li>
 *   <li>Load all schema files located in the schema directory.</li>
 * </ul>
 */
public final class SchemaHandler
{
  private static final String CORE_SCHEMA_PROVIDER_NAME = "Core Schema";
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  private ServerContext serverContext;
  private long oldestModificationTime = -1L;
  private long youngestModificationTime = -1L;
  /**
   * Creates a new instance.
   */
  public SchemaHandler()
  {
    // no implementation.
  }
  /**
   * Initialize this schema handler.
   *
   * @param serverContext
   *          The server context.
   * @throws ConfigException
   *           If a configuration problem arises in the process of performing
   *           the initialization.
   * @throws InitializationException
   *           If a problem that is not configuration-related occurs during
   *           initialization.
   */
  public void initialize(final ServerContext serverContext) throws InitializationException, ConfigException
  {
    this.serverContext = serverContext;
    final RootCfg rootConfiguration = serverContext.getServerManagementContext().getRootConfiguration();
    final SchemaUpdater schemaUpdater = serverContext.getSchemaUpdater();
    // Start from the core schema (TODO: or start with empty schema and add core schema in core schema provider ?)
    final SchemaBuilder schemaBuilder = new SchemaBuilder(Schema.getCoreSchema());
    // Take providers into account.
    loadSchemaFromProviders(rootConfiguration, schemaBuilder, schemaUpdater);
    // Take schema files into account (TODO : or load files using provider mechanism ?)
    completeSchemaFromFiles(schemaBuilder);
    schemaUpdater.updateSchema(schemaBuilder);
  }
  /**
   * Load the schema from provided root configuration.
   *
   * @param rootConfiguration
   *          The root to retrieve schema provider configurations.
   * @param schemaBuilder
   *          The schema builder that providers should update.
   * @param schemaUpdater
   *          The updater that providers should use when applying a
   *          configuration change.
   */
  private void loadSchemaFromProviders(final RootCfg rootConfiguration, final SchemaBuilder schemaBuilder,
      final SchemaUpdater schemaUpdater)  throws ConfigException, InitializationException {
    for (final String name : rootConfiguration.listSchemaProviders())
    {
      final SchemaProviderCfg config = rootConfiguration.getSchemaProvider(name);
      if (config.isEnabled())
      {
        loadSchemaProvider(config.getJavaClass(), config, schemaBuilder, schemaUpdater, true);
      }
      else if (name.equals(CORE_SCHEMA_PROVIDER_NAME)) {
        // TODO : use correct message ERR_CORE_SCHEMA_NOT_ENABLED
        LocalizableMessage message = LocalizableMessage.raw("Core Schema can't be disabled");
        throw new ConfigException(message);
      }
    }
  }
  /**
   * Load the schema provider from the provided class name.
   * <p>
   * If {@code} initialize} is {@code true}, then the provider is initialized,
   * and the provided schema builder is updated with schema elements fropm the
   * provider.
   */
  private <T extends SchemaProviderCfg> SchemaProvider<T> loadSchemaProvider(final String className,
      final T config, final SchemaBuilder schemaBuilder, final SchemaUpdater schemaUpdater, final boolean initialize)
      throws InitializationException
  {
    try
    {
      final ClassPropertyDefinition propertyDef = SchemaProviderCfgDefn.getInstance().getJavaClassPropertyDefinition();
      final Class<? extends SchemaProvider> providerClass = propertyDef.loadClass(className, SchemaProvider.class);
      final SchemaProvider<T> provider = providerClass.newInstance();
      if (initialize) {
        provider.initialize(config, schemaBuilder, schemaUpdater);
      }
      else {
        final List<LocalizableMessage> unacceptableReasons = new ArrayList<LocalizableMessage>();
        final boolean isAcceptable = provider.isConfigurationAcceptable(config, unacceptableReasons);
        if (!isAcceptable)
        {
          final String reasons = Utils.joinAsString(".  ", unacceptableReasons);
          // TODO : fix message, eg CONFIG SCHEMA PROVIDER CONFIG NOT ACCEPTABLE
          throw new InitializationException(ERR_CONFIG_ALERTHANDLER_CONFIG_NOT_ACCEPTABLE.get(config.dn(), reasons));
        }
      }
      return provider;
    }
    catch (Exception e)
      {
        // TODO : fix message
        throw new InitializationException(ERR_CONFIG_SCHEMA_SYNTAX_CANNOT_INITIALIZE.
            get(className, config.dn(), stackTraceToSingleLineString(e)), e);
      }
  }
  /**
   * Retrieves the path to the directory containing the server schema files.
   *
   * @return The path to the directory containing the server schema files.
   */
  private File getSchemaDirectoryPath() throws InitializationException
  {
    final File dir = serverContext.getEnvironment().getSchemaDirectory();
    if (dir == null)
    {
      throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(null));
    }
    if (!dir.exists())
    {
      throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(dir.getPath()));
    }
    if (!dir.isDirectory())
    {
      throw new InitializationException(ERR_CONFIG_SCHEMA_DIR_NOT_DIRECTORY.get(dir.getPath()));
    }
    return dir;
  }
  /** Returns the LDIF reader on provided LDIF file. The caller must ensure the reader is closed. */
  private EntryReader getLDIFReader(final File ldifFile, final Schema schema)
      throws InitializationException
  {
    try
    {
      final LDIFEntryReader reader = new LDIFEntryReader(new FileReader(ldifFile));
      reader.setSchema(schema);
      return reader;
    }
    catch (Exception e)
    {
      // TODO : fix message
      throw new InitializationException(ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(ldifFile.getAbsolutePath(), e), e);
    }
  }
  /**
   * Complete the schema with schema files.
   *
   * @param schemaBuilder
   *          The schema builder to update with the content of the schema files.
   * @throws ConfigException
   *           If a configuration problem causes the schema element
   *           initialization to fail.
   * @throws InitializationException
   *           If a problem occurs while initializing the schema elements that
   *           is not related to the server configuration.
   */
  private void completeSchemaFromFiles(final SchemaBuilder schemaBuilder)
      throws ConfigException, InitializationException
  {
    final File schemaDirectory = getSchemaDirectoryPath();
    for (String schemaFile : getSchemaFileNames(schemaDirectory))
    {
      loadSchemaFile(schemaFile, schemaBuilder, Schema.getDefaultSchema());
    }
  }
  /** Returns the list of names of schema files contained in the provided directory. */
  private List<String> getSchemaFileNames(final File schemaDirectory) throws InitializationException {
    try
    {
      final File[] schemaFiles = schemaDirectory.listFiles(new SchemaFileFilter());
      final List<String> schemaFileNames = new ArrayList<String>(schemaFiles.length);
      for (final File f : schemaFiles)
      {
        if (f.isFile())
        {
          schemaFileNames.add(f.getName());
        }
        final long modificationTime = f.lastModified();
        if (oldestModificationTime <= 0L
            || modificationTime < oldestModificationTime)
        {
          oldestModificationTime = modificationTime;
        }
        if (youngestModificationTime <= 0
            || modificationTime > youngestModificationTime)
        {
          youngestModificationTime = modificationTime;
        }
      }
      // If the oldest and youngest modification timestamps didn't get set
      // then set them to the current time.
      if (oldestModificationTime <= 0)
      {
        oldestModificationTime = System.currentTimeMillis();
      }
      if (youngestModificationTime <= 0)
      {
        youngestModificationTime = oldestModificationTime;
      }
      Collections.sort(schemaFileNames);
      return schemaFileNames;
    }
    catch (Exception e)
    {
      throw new InitializationException(ERR_CONFIG_SCHEMA_CANNOT_LIST_FILES
          .get(schemaDirectory, getExceptionMessage(e)), e);
    }
  }
  /** Returns the schema entry from the provided reader. */
  private Entry readSchemaEntry(final EntryReader reader, final File schemaFile) throws InitializationException {
    try
    {
      Entry entry = null;
      if (reader.hasNext())
      {
        entry = reader.readEntry();
        if (reader.hasNext())
        {
          // TODO : fix message
          logger.warn(WARN_CONFIG_SCHEMA_MULTIPLE_ENTRIES_IN_FILE, schemaFile, "");
        }
        return entry;
      }
      else
      {
        // TODO : fix message - should be SCHEMA NO LDIF ENTRY
        throw new InitializationException(WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get(
            schemaFile, "", ""));
      }
    }
    catch (IOException e)
    {
      // TODO : fix message
      throw new InitializationException(WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get(
              schemaFile, "", getExceptionMessage(e)), e);
    }
    finally
    {
      closeSilently(reader);
    }
  }
  /**
   * Add the schema from the provided schema file to the provided schema
   * builder.
   *
   * @param schemaFileName
   *          The name of the schema file to be loaded
   * @param schemaBuilder
   *          The schema builder in which the contents of the schema file are to
   *          be loaded.
   * @param readSchema
   *          The schema used to read the file.
   * @throws InitializationException
   *           If a problem occurs while initializing the schema elements.
   */
  private void loadSchemaFile(final String schemaFileName, final SchemaBuilder schemaBuilder, final Schema readSchema)
         throws InitializationException
  {
    EntryReader reader = null;
    try
    {
      File schemaFile = new File(getSchemaDirectoryPath(), schemaFileName);
      reader = getLDIFReader(schemaFile, readSchema);
      final Entry entry = readSchemaEntry(reader, schemaFile);
      // TODO : there is no more file information attached to schema elements - we should add support for this
      // in order to be able to redirect schema elements in the correct file when doing backups
      schemaBuilder.addSchema(entry, true);
    }
    finally {
      Utils.closeSilently(reader);
    }
  }
  /** A file filter implementation that accepts only LDIF files. */
  private static class SchemaFileFilter implements FilenameFilter
  {
    private static final String LDIF_SUFFIX = ".ldif";
    @Override
    public boolean accept(File directory, String filename)
    {
      return filename.endsWith(LDIF_SUFFIX);
    }
  }
}
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/core/ServerContext.java
@@ -25,7 +25,8 @@
 */
package org.opends.server.core;
import org.opends.server.admin.server.ServerManagementContext;
import org.forgerock.opendj.config.server.ServerManagementContext;
import org.opends.server.schema.SchemaUpdater;
import org.opends.server.types.DirectoryEnvironmentConfig;
import org.opends.server.types.Schema;
@@ -57,6 +58,14 @@
  public Schema getSchema();
  /**
   * Returns the schema updater, which provides
   * a mean to update the server's current schema.
   *
   * @return the schema updater
   */
  public SchemaUpdater getSchemaUpdater();
  /**
   * Returns the environment of the server.
   *
   * @return the environment
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/schema/CoreSchemaProvider.java
New file
@@ -0,0 +1,156 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS.
 */
package org.opends.server.schema;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.opendj.server.config.server.CoreSchemaCfg;
import org.opends.server.config.ConfigException;
import org.opends.server.types.InitializationException;
/**
 * Provides the core schema, which includes core matching rules and syntaxes.
 */
public class CoreSchemaProvider implements SchemaProvider<CoreSchemaCfg>,
  ConfigurationChangeListener<CoreSchemaCfg>
{
  private static final String NONE_ELEMENT = "NONE";
  /** The current configuration of core schema. */
  private CoreSchemaCfg currentConfig;
  /** The current schema builder. */
  private SchemaBuilder currentSchemaBuilder;
  /** Updater to notify schema update when configuration changes. */
  private SchemaUpdater schemaUpdater;
  /** {@inheritDoc} */
  @Override
  public void initialize(final CoreSchemaCfg configuration, final SchemaBuilder initialSchemaBuilder,
      final SchemaUpdater schemaUpdater) throws ConfigException, InitializationException
  {
    this.currentConfig = configuration;
    this.currentSchemaBuilder = initialSchemaBuilder;
    this.schemaUpdater = schemaUpdater;
    updateSchemaFromConfiguration(initialSchemaBuilder, configuration);
    currentConfig.addCoreSchemaChangeListener(this);
  }
  /**
   * Update the provided schema builder with the provided configuration.
   *
   * @param schemaBuilder
   *          The schema builder to update.
   * @param configuration
   *          The configuration to use for update.
   */
  private void updateSchemaFromConfiguration(final SchemaBuilder schemaBuilder, final CoreSchemaCfg configuration)
  {
    schemaBuilder.allowZeroLengthDirectoryStrings(configuration.isAllowZeroLengthValuesDirectoryString());
    // TODO : add the missing methods in schema builder for those properties
    // schemaBuilder.allowMalformedJPEGPhotos(configuration.)
    // schemaBuilder.allowMalformedNamesAndOptions(configuration.)
    // ...
    for (final String oid : configuration.getDisabledMatchingRule())
    {
      if (oid.equals(NONE_ELEMENT)) {
          break;
      }
      schemaBuilder.removeMatchingRule(oid);
    }
    for (final String oid : configuration.getDisabledSyntax())
    {
      if (oid.equals(NONE_ELEMENT)) {
        break;
      }
      schemaBuilder.removeSyntax(oid);
    }
  }
  /** {@inheritDoc} */
  @Override
  public void finalizeProvider()
  {
    currentConfig.removeCoreSchemaChangeListener(this);
  }
  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationAcceptable(final CoreSchemaCfg configuration,
      final List<LocalizableMessage> unacceptableReasons)
  {
    // TODO : check that elements to disable are present in the schema ?
    return true;
  }
  /** {@inheritDoc} */
  @Override
  public SchemaBuilder getSchema()
  {
    return currentSchemaBuilder;
  }
  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationChangeAcceptable(final CoreSchemaCfg configuration,
      final List<LocalizableMessage> unacceptableReasons)
  {
    if (!configuration.isEnabled())
    {
      // TODO : fix message
      unacceptableReasons.add(LocalizableMessage.raw("The core schema must always be enabled"));
      return false;
    }
    // TODO : check that elements to disable are present in the schema ?
    return true;
  }
  /** {@inheritDoc} */
  @Override
  public ConfigChangeResult applyConfigurationChange(final CoreSchemaCfg configuration)
  {
    currentSchemaBuilder = schemaUpdater.getSchemaBuilder();
    updateSchemaFromConfiguration(currentSchemaBuilder, configuration);
    final boolean isUpdated = schemaUpdater.updateSchema(currentSchemaBuilder);
    // TODO : fix result code + log an error in case of failure
    final ResultCode code = isUpdated ? ResultCode.SUCCESS : ResultCode.OTHER;
    return new ConfigChangeResult(code, false);
  }
}
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/schema/SchemaProvider.java
New file
@@ -0,0 +1,93 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS.
 */
package org.opends.server.schema;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.opendj.server.config.server.SchemaProviderCfg;
import org.opends.server.config.ConfigException;
import org.opends.server.types.InitializationException;
/**
 * Provides some schema elements to load at startup.
 * <p>
 * A schema provider must be able to update the provided {@code SchemaBuilder}
 * at initialization and to use the provided {@code SchemaUpdater} to update the
 * schema on further configuration changes.
 *
 * @param <T>
 *          The type of provider configuration.
 */
public interface SchemaProvider<T extends SchemaProviderCfg>
{
  /**
   * Initialize the schema provider from provided configuration and schema
   * builder.
   *
   * @param configuration
   *          Configuration of the provider.
   * @param initialSchemaBuilder
   *          Schema builder to update during initialization phase.
   * @param schemaUpdater
   *          The updater to call when applying a configuration change.
   * @throws ConfigException
   *           If a configuration problem arises in the process of performing
   *           the initialization.
   * @throws InitializationException
   *           If a problem that is not configuration-related occurs during
   *           initialization.
   */
  void initialize(T configuration, SchemaBuilder initialSchemaBuilder, SchemaUpdater schemaUpdater)
      throws ConfigException, InitializationException;
  /**
   * Finalize the provider.
   */
  void finalizeProvider();
  /**
   * Indicates whether the provided configuration is acceptable for this
   * provider.
   *
   * @param configuration
   *          The provider configuration for which to make the determination.
   * @param unacceptableReasons
   *          A list that may be used to hold the reasons that the provided
   *          configuration is not acceptable.
   * @return {@code true} if the provided configuration is acceptable for this
   *         provider, or {@code false} if not.
   */
  boolean isConfigurationAcceptable(T configuration, List<LocalizableMessage> unacceptableReasons);
  /**
   * Returns the schema builder, as updated by this provider.
   *
   * @return the schema builder resulting from this provider.
   */
  SchemaBuilder getSchema();
}
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/schema/SchemaUpdater.java
New file
@@ -0,0 +1,51 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS.
 */
package org.opends.server.schema;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
/**
 * An interface to update a schema through a schema builder.
 */
public interface SchemaUpdater
{
  /**
   * Returns a schema builder based on the current schema.
   *
   * @return a schema builder
   */
  SchemaBuilder getSchemaBuilder();
  /**
   * Update the current schema with the schema built from the
   * provided schema builder.
   *
   * @param schemaBuilder
   *          Builder representing the updated schema.
   * @return {@code true} if the update succeeded, false otherwise
   */
  boolean updateSchema(SchemaBuilder schemaBuilder);
}
opendj-sdk/opendj3-server-dev/src/server/org/opends/server/types/DirectoryEnvironmentConfig.java
@@ -61,6 +61,8 @@
  /** The set of properties for the environment config. */
  private final Map<String, String> configProperties;
  private final boolean checkIfServerIsRunning;
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /**
@@ -69,7 +71,19 @@
   */
  public DirectoryEnvironmentConfig()
  {
    this(System.getProperties());
    this(true);
  }
  /**
   * Creates a new directory environment configuration initialized from the
   * system properties defined in the JVM.
   *
   * @param checkIfServerIsRunning
   *          If {@code true}, prevent any change when server is running.
   */
  public DirectoryEnvironmentConfig(boolean checkIfServerIsRunning)
  {
    this(System.getProperties(), checkIfServerIsRunning);
  }
@@ -81,9 +95,12 @@
   * @param  properties  The properties to use when initializing this
   *                     environment configuration, or {@code null}
   *                     to use an empty set of properties.
   * @param checkIfServerIsRunning
   *            If {@code true}, prevent any change when server is running.
   */
  public DirectoryEnvironmentConfig(Properties properties)
  public DirectoryEnvironmentConfig(Properties properties, boolean checkIfServerIsRunning)
  {
    this.checkIfServerIsRunning = checkIfServerIsRunning;
    configProperties = new HashMap<String,String>();
    if (properties != null)
    {
@@ -106,9 +123,12 @@
   * @param  properties  The properties to use when initializing this
   *                     environment configuration, or {@code null}
   *                     to use an empty set of properties.
   * @param checkIfServerIsRunning
   *            If {@code true}, prevent any change when server is running.
   */
  public DirectoryEnvironmentConfig(Map<String,String> properties)
  public DirectoryEnvironmentConfig(Map<String,String> properties, boolean checkIfServerIsRunning)
  {
    this.checkIfServerIsRunning = checkIfServerIsRunning;
    if (properties == null)
    {
      configProperties = new HashMap<String,String>();
@@ -163,11 +183,7 @@
  public String setProperty(String name, String value)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (value == null)
    {
@@ -352,11 +368,7 @@
  public File setServerRoot(File serverRoot)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (!serverRoot.exists() || !serverRoot.isDirectory())
    {
@@ -420,11 +432,7 @@
  public File setInstanceRoot(File instanceRoot)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (!instanceRoot.exists() || !instanceRoot.isDirectory())
    {
@@ -493,11 +501,7 @@
  public File setConfigFile(File configFile)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (!configFile.exists() || !configFile.isFile())
    {
@@ -563,11 +567,7 @@
  public Class setConfigClass(Class configClass)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (!ConfigHandler.class.isAssignableFrom(configClass))
    {
@@ -696,11 +696,7 @@
                      boolean maintainConfigArchive)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    String oldMaintainStr =
         setProperty(PROPERTY_MAINTAIN_CONFIG_ARCHIVE,
@@ -768,11 +764,7 @@
  public int setMaxConfigArchiveSize(int maxConfigArchiveSize)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (maxConfigArchiveSize < 0)
    {
@@ -864,11 +856,7 @@
  public File setSchemaDirectory(File schemaDirectory)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (!schemaDirectory.exists() || !schemaDirectory.isDirectory())
    {
@@ -936,11 +924,7 @@
  public File setLockDirectory(File lockDirectory)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (lockDirectory.exists())
    {
@@ -1049,11 +1033,7 @@
  private boolean setBooleanProperty(String propertyName, boolean newValue)
      throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    final String oldValue = setProperty(propertyName, String.valueOf(newValue));
    return "true".equalsIgnoreCase(oldValue);
@@ -1195,11 +1175,7 @@
  public int setLockManagerConcurrencyLevel(int concurrencyLevel)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (concurrencyLevel <= 0)
    {
@@ -1271,11 +1247,7 @@
  public boolean setLockManagerFairOrdering(boolean fairOrdering)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    String fairOrderingStr =
         setProperty(PROPERTY_LOCK_MANAGER_FAIR_ORDERING,
@@ -1297,6 +1269,16 @@
    }
  }
  /** Throws an exception if server is running and it is not allowed. */
  private void checkServerIsRunning() throws InitializationException
  {
    if (checkIfServerIsRunning && DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
  }
  /**
   * Retrieves the initial table size for the server lock table.  This
   * can be used to ensure that the lock table has the appropriate
@@ -1348,11 +1330,7 @@
  public int setLockManagerTableSize(int lockTableSize)
         throws InitializationException
  {
    if (DirectoryServer.isRunning())
    {
      throw new InitializationException(
              ERR_DIRCFG_SERVER_ALREADY_RUNNING.get());
    }
    checkServerIsRunning();
    if (lockTableSize <= 0)
    {
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/resource/config-small.ldif
New file
@@ -0,0 +1,86 @@
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License, Version 1.0 only
# (the "License").  You may not use this file except in compliance
# with the License.
#
# You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
# or http://forgerock.org/license/CDDLv1.0.html.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at legal-notices/CDDLv1_0.txt.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information:
#      Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#      Copyright 2006-2010 Sun Microsystems, Inc.
#      Portions Copyright 2010-2014 ForgeRock AS.
#      Portions Copyright 2012-2014 Manuel Gaupp
#
#
# This file contains the primary Directory Server configuration.  It must not
# be directly edited while the server is online.  The server configuration
# should only be managed using the administration utilities provided with the
# Directory Server.
dn: cn=config
objectClass: top
objectClass: ds-cfg-root-config
cn: config
ds-cfg-check-schema: true
ds-cfg-add-missing-rdn-attributes: true
ds-cfg-allow-attribute-name-exceptions: false
ds-cfg-invalid-attribute-syntax-behavior: reject
ds-cfg-single-structural-objectclass-behavior: reject
ds-cfg-notify-abandoned-operations: false
ds-cfg-proxied-authorization-identity-mapper: cn=Exact Match,cn=Identity Mappers,cn=config
ds-cfg-size-limit: 1000
ds-cfg-time-limit: 60 seconds
ds-cfg-lookthrough-limit: 5000
ds-cfg-writability-mode: enabled
ds-cfg-bind-with-dn-requires-password: true
ds-cfg-reject-unauthenticated-requests: false
ds-cfg-default-password-policy: cn=Default Password Policy,cn=Password Policies,cn=config
ds-cfg-return-bind-error-messages: false
ds-cfg-idle-time-limit: 0 seconds
ds-cfg-save-config-on-successful-startup: true
ds-cfg-etime-resolution: milliseconds
ds-cfg-entry-cache-preload: false
ds-cfg-max-allowed-client-connections: 0
ds-cfg-max-psearches: -1
ds-cfg-allowed-task: org.opends.server.tasks.AddSchemaFileTask
ds-cfg-allowed-task: org.opends.server.tasks.BackupTask
ds-cfg-allowed-task: org.opends.server.tasks.DisconnectClientTask
ds-cfg-allowed-task: org.opends.server.tasks.EnterLockdownModeTask
ds-cfg-allowed-task: org.opends.server.tasks.ExportTask
ds-cfg-allowed-task: org.opends.server.tasks.ImportTask
ds-cfg-allowed-task: org.opends.server.tasks.InitializeTargetTask
ds-cfg-allowed-task: org.opends.server.tasks.InitializeTask
ds-cfg-allowed-task: org.opends.server.tasks.SetGenerationIdTask
ds-cfg-allowed-task: org.opends.server.tasks.LeaveLockdownModeTask
ds-cfg-allowed-task: org.opends.server.tasks.RebuildTask
ds-cfg-allowed-task: org.opends.server.tasks.RestoreTask
ds-cfg-allowed-task: org.opends.server.tasks.ShutdownTask
ds-cfg-allowed-task: org.opends.server.tasks.PurgeConflictsHistoricalTask
dn: cn=Schema Providers,cn=config
objectClass: top
objectClass: ds-cfg-branch
cn: Schema Providers
dn: cn=Core Schema,cn=Schema Providers,cn=config
objectClass: top
objectClass: ds-cfg-schema-provider
objectClass: ds-cfg-core-schema
ds-cfg-java-class: org.opends.server.schema.CoreSchemaProvider
ds-cfg-enabled: true
ds-cfg-disabled-matching-rule: NONE
ds-cfg-disabled-syntax: NONE
ds-cfg-strip-syntax-min-upper-bound-attribute-type-description: false
ds-cfg-strict-format-country-string: false
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/ConfigurationMock.java
New file
@@ -0,0 +1,246 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2013-2014 ForgeRock AS.
 */
package org.opends.server;
import static org.mockito.Mockito.mock;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.SortedSet;
import java.util.TreeSet;
import org.forgerock.opendj.config.AbsoluteInheritedDefaultBehaviorProvider;
import org.forgerock.opendj.config.AliasDefaultBehaviorProvider;
import org.forgerock.opendj.config.Configuration;
import org.forgerock.opendj.config.DefaultBehaviorProvider;
import org.forgerock.opendj.config.DefaultBehaviorProviderVisitor;
import org.forgerock.opendj.config.DefinedDefaultBehaviorProvider;
import org.forgerock.opendj.config.ManagedObjectDefinition;
import org.forgerock.opendj.config.PropertyDefinition;
import org.forgerock.opendj.config.PropertyOption;
import org.forgerock.opendj.config.RelativeInheritedDefaultBehaviorProvider;
import org.forgerock.opendj.config.UndefinedDefaultBehaviorProvider;
import org.mockito.internal.stubbing.defaultanswers.ReturnsEmptyValues;
import org.mockito.invocation.InvocationOnMock;
/**
 * Provides Mockito mocks for Configuration objects with default values
 * corresponding to those defined in xml configuration files.
 * <p>
 * These mocks can be used like any other mocks, e.g, you can define stubs using
 * {@code when} method or verify calls using {@code verify} method.
 * <p>
 * Example:
 *
 * <pre>
 *  LDAPConnectionHandlerCfg mockCfg = mockCfg(LDAPConnectionHandlerCfg.class);
 *  assertThat(mockCfg.getMaxRequestSize()).isEqualTo(5 * 1000 * 1000);
 * </pre>
 */
public final class ConfigurationMock {
    private static final ConfigAnswer CONFIG_ANSWER = new ConfigAnswer();
    /**
     * Returns a mock for the provided configuration class.
     * <p>
     * If a setting has a default value, the mock automatically returns the
     * default value when the getter is called on the setting.
     * <p>
     * It is possible to override this default behavior with the usual methods
     * calls with Mockito (e.g, {@code when} method).
     *
     * @param <T>
     *            The type of configuration.
     * @param configClass
     *            The configuration class.
     * @return a mock
     */
    public static <T extends Configuration> T mockCfg(Class<T> configClass) {
        return mock(configClass, CONFIG_ANSWER);
    }
    /**
     * A stubbed answer for Configuration objects, allowing to return default
     * value for settings when available.
     */
    private static class ConfigAnswer extends ReturnsEmptyValues {
        private static final long serialVersionUID = 1L;
        /** {@inheritDoc} */
        @Override
        public Object answer(InvocationOnMock invocation) {
            try {
                String definitionClassName =
                    toDefinitionClassName(invocation.getMethod().getDeclaringClass().getName());
                Class<?> definitionClass = Class.forName(definitionClassName);
                ManagedObjectDefinition<?, ?> definition =
                    (ManagedObjectDefinition<?, ?>) definitionClass.getMethod("getInstance").invoke(null);
                String invokedMethodName = invocation.getMethod().getName();
                if (!isGetterMethod(invokedMethodName)) {
                    return answerFromDefaultMockitoBehavior(invocation);
                }
                Method getPropertyDefMethod = getPropertyDefinitionMethod(definitionClass, invokedMethodName);
                Class<?> propertyReturnType = getPropertyReturnType(getPropertyDefMethod);
                Object defaultValue = getDefaultValue(definition, getPropertyDefMethod, propertyReturnType);
                if (defaultValue == null) {
                    return answerFromDefaultMockitoBehavior(invocation);
                }
                return defaultValue;
            } catch (Exception e) {
                return answerFromDefaultMockitoBehavior(invocation);
            }
        }
        private Object answerFromDefaultMockitoBehavior(InvocationOnMock invocation) {
            return super.answer(invocation);
        }
        private boolean isGetterMethod(String invokedMethodName) {
            return invokedMethodName.startsWith("get") || invokedMethodName.startsWith("is");
        }
        private Method getPropertyDefinitionMethod(Class<?> definitionClass, String invokedMethodName)
                throws SecurityException, NoSuchMethodException {
            // Methods for boolean starts with "is" in Cfg class but with "get" in CfgDefn class.
            return definitionClass.getMethod(invokedMethodName.replaceAll("^is", "get") + "PropertyDefinition");
        }
        /**
         * Returns the type of values returned by the property.
         */
        private Class<?> getPropertyReturnType(Method getPropertyDefMethod) {
            Class<?> returnClass = getPropertyDefMethod.getReturnType();
            return ((ParameterizedType) returnClass.getGenericSuperclass())
                    .getActualTypeArguments()[0].getClass();
        }
        /**
         * Retrieve class name of definition from class name of configuration.
         * <p>
         * Convert class name "[package].server.FooCfg" to "[package].meta.FooCfgDef"
         */
        private String toDefinitionClassName(String configClassName) {
            int finalDot = configClassName.lastIndexOf('.');
            return configClassName.substring(0, finalDot - 6) + "meta."
                    + configClassName.substring(finalDot + 1) + "Defn";
        }
        /**
         * Returns the default value corresponding to the provided property
         * definition getter method from the provided managed object definition.
         *
         * @param <T>
         *            The data type of values provided by the property
         *            definition.
         * @param definition
         *            The definition of the managed object.
         * @param getPropertyDefMethod
         *            The method to retrieve the property definition from the
         *            definition.
         * @param propertyReturnClass
         *            The class of values provided by the property definition.
         * @return the default value of property definition, or
         *         {@code null} if there is no default value.
         * @throws Exception
         *             If an error occurs.
         */
        @SuppressWarnings("unchecked")
        private <T> Object getDefaultValue(ManagedObjectDefinition<?, ?> definition,
                Method getPropertyDefMethod, Class<T> propertyReturnClass)
                throws Exception {
            PropertyDefinition<T> propertyDefinition = (PropertyDefinition<T>) getPropertyDefMethod.invoke(definition);
            DefaultBehaviorProvider<T> defaultBehaviorProvider = (DefaultBehaviorProvider<T>) propertyDefinition
                    .getClass().getMethod("getDefaultBehaviorProvider").invoke(propertyDefinition);
            MockProviderVisitor<T> visitor = new MockProviderVisitor<T>(propertyDefinition);
            Collection<T> values = defaultBehaviorProvider.accept(visitor, null);
            if (values == null) {
                // No default behavior defined
                return null;
            } else if (propertyDefinition.hasOption(PropertyOption.MULTI_VALUED)) {
                return values;
            } else {
                // Single value returned
                return values.iterator().next();
            }
        }
    }
    /** Visitor used to retrieve the default value. */
    private static class MockProviderVisitor<T> implements DefaultBehaviorProviderVisitor<T, Collection<T>, Void> {
        /** The property definition used to decode the values. */
        private PropertyDefinition<T> propertyDef;
        MockProviderVisitor(PropertyDefinition<T> propertyDef) {
            this.propertyDef = propertyDef;
        }
        /** {@inheritDoc} */
        @Override
        public Collection<T> visitAbsoluteInherited(AbsoluteInheritedDefaultBehaviorProvider<T> provider, Void p) {
            // not handled
            return null;
        }
        /** {@inheritDoc} */
        @Override
        public Collection<T> visitAlias(AliasDefaultBehaviorProvider<T> provider, Void p) {
            // not handled
            return null;
        }
        /**
         * Returns the default value for the property as a collection.
         */
        @Override
        public Collection<T> visitDefined(DefinedDefaultBehaviorProvider<T> provider, Void p) {
            SortedSet<T> values = new TreeSet<T>();
            for (String stringValue : provider.getDefaultValues()) {
                values.add(propertyDef.decodeValue(stringValue));
            }
            return values;
        }
        /** {@inheritDoc} */
        @Override
        public Collection<T> visitRelativeInherited(RelativeInheritedDefaultBehaviorProvider<T> d, Void p) {
            // not handled
            return null;
        }
        /** {@inheritDoc} */
        @Override
        public Collection<T> visitUndefined(UndefinedDefaultBehaviorProvider<T> d, Void p) {
            // not handled
            return null;
        }
    }
}
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/ServerContextBuilder.java
New file
@@ -0,0 +1,103 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS.
 */
package org.opends.server;
import static org.mockito.Mockito.*;
import java.io.File;
import org.forgerock.opendj.config.server.ServerManagementContext;
import org.opends.server.core.ConfigurationBootstrapper;
import org.opends.server.core.ServerContext;
import org.opends.server.schema.SchemaUpdater;
import org.opends.server.types.DirectoryEnvironmentConfig;
import org.opends.server.types.InitializationException;
/**
 * A server context builder to be used in tests to minimize arrange phase.
 */
@SuppressWarnings("javadoc")
public class ServerContextBuilder
{
  private final ServerContext serverContext;
  private final DirectoryEnvironmentConfig env;
  public static ServerContextBuilder aServerContext()
  {
    return new ServerContextBuilder();
  }
  public ServerContextBuilder()
  {
    serverContext = mock(ServerContext.class);
    env = new DirectoryEnvironmentConfig(false);
    when(serverContext.getEnvironment()).thenReturn(env);
  }
  public ServerContext build()
  {
    return serverContext;
  }
  public ServerContextBuilder setSchemaDirectory(File path)
      throws InitializationException
  {
    env.setSchemaDirectory(path);
    return this;
  }
  public ServerContextBuilder setConfigFile(File path)
      throws InitializationException
  {
    env.setConfigFile(path);
    return this;
  }
  public ServerContextBuilder setSchemaUpdater(SchemaUpdater updater)
  {
    when(serverContext.getSchemaUpdater()).thenReturn(updater);
    return this;
  }
  /**
   * Ensure that configuration is fully bootstrapped. Only use when necessary as
   * it will impact test performance.
   */
  public ServerContextBuilder withConfigurationBootstrapped()
      throws InitializationException
  {
    if (serverContext.getSchemaUpdater() == null) {
      throw new RuntimeException("You must set a non-null schema updater to bootstrap configuration.");
    }
    final ServerManagementContext serverManagementContext =
        ConfigurationBootstrapper.bootstrap(serverContext);
    when(serverContext.getServerManagementContext()).thenReturn(
        serverManagementContext);
    return this;
  }
}
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/core/ConfigurationHandlerTestCase.java
New file
@@ -0,0 +1,395 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS.
 */
package org.opends.server.core;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import static org.opends.server.ServerContextBuilder.*;
import static org.testng.Assert.*;
import java.io.File;
import java.util.Arrays;
import java.util.Set;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.spi.ConfigAddListener;
import org.forgerock.opendj.config.server.spi.ConfigChangeListener;
import org.forgerock.opendj.config.server.spi.ConfigDeleteListener;
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.opends.server.TestCaseUtils;
import org.opends.server.config.ConfigConstants;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.testng.annotations.Test;
@SuppressWarnings("javadoc")
public class ConfigurationHandlerTestCase extends CoreTestCase
{
  private static final DN DN_CONFIG = DN.valueOf(ConfigConstants.DN_CONFIG_ROOT);
  private static final DN DN_SCHEMA_PROVIDERS = DN.valueOf("cn=Schema Providers,cn=config");
  private static final DN DN_CORE_SCHEMA = DN.valueOf("cn=Core Schema,cn=Schema Providers,cn=config");
  /** Returns the configuration handler fully initialized from configuration file. */
  private ConfigurationHandler getConfigurationHandler()
      throws InitializationException
  {
    final ServerContext context = aServerContext().
        setSchemaDirectory(new File(TestCaseUtils.getBuildRoot(), "resource/schema")).
        setConfigFile(TestCaseUtils.getTestResource("config-small.ldif")).
        build();
    final ConfigurationHandler configHandler = new ConfigurationHandler(context);
    configHandler.initialize();
    return configHandler;
  }
  @Test
  public void testInitializeConfiguration() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    assertTrue(configHandler.hasEntry(DN_CONFIG));
  }
  @Test
  public void testGetEntry() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    Entry entry = configHandler.getEntry(DN_SCHEMA_PROVIDERS);
    assertTrue(entry.containsAttribute("objectclass", "top", "ds-cfg-branch"));
  }
  @Test
  public void testGetChildren() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    Set<DN> dns = configHandler.getChildren(DN_SCHEMA_PROVIDERS);
    assertTrue(dns.contains(DN_CORE_SCHEMA));
  }
  @Test
  public void testNumSubordinates() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    long numSubordinates = configHandler.numSubordinates(DN_SCHEMA_PROVIDERS, false);
    assertEquals(numSubordinates, 1);
    numSubordinates = configHandler.numSubordinates(DN_CONFIG, true);
    assertEquals(numSubordinates, 2);
  }
  @Test
  public void testRegisterChangeListener() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigChangeListener listener1 = mock(ConfigChangeListener.class);
    ConfigChangeListener listener2 = mock(ConfigChangeListener.class);
    configHandler.registerChangeListener(DN_SCHEMA_PROVIDERS, listener1);
    configHandler.registerChangeListener(DN_SCHEMA_PROVIDERS, listener2);
    assertEquals(configHandler.getChangeListeners(DN_SCHEMA_PROVIDERS), Arrays.asList(listener1 ,listener2));
  }
  @Test
  public void testRegisterDeregisterChangeListener() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigChangeListener listener1 = mock(ConfigChangeListener.class);
    ConfigChangeListener listener2 = mock(ConfigChangeListener.class);
    configHandler.registerChangeListener(DN_SCHEMA_PROVIDERS, listener1);
    configHandler.registerChangeListener(DN_SCHEMA_PROVIDERS, listener2);
    configHandler.deregisterChangeListener(DN_SCHEMA_PROVIDERS, listener1);
    assertEquals(configHandler.getChangeListeners(DN_SCHEMA_PROVIDERS), Arrays.asList(listener2));
  }
  @Test
  public void testRegisterAddListener() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigAddListener listener1 = mock(ConfigAddListener.class);
    ConfigAddListener listener2 = mock(ConfigAddListener.class);
    configHandler.registerAddListener(DN_SCHEMA_PROVIDERS, listener1);
    configHandler.registerAddListener(DN_SCHEMA_PROVIDERS, listener2);
    assertEquals(configHandler.getAddListeners(DN_SCHEMA_PROVIDERS), Arrays.asList(listener1 ,listener2));
  }
  @Test
  public void testRegisterDeleteListener() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigDeleteListener listener1 = mock(ConfigDeleteListener.class);
    ConfigDeleteListener listener2 = mock(ConfigDeleteListener.class);
    configHandler.registerDeleteListener(DN_SCHEMA_PROVIDERS, listener1);
    configHandler.registerDeleteListener(DN_SCHEMA_PROVIDERS, listener2);
    assertEquals(configHandler.getDeleteListeners(DN_SCHEMA_PROVIDERS), Arrays.asList(listener1 ,listener2));
  }
  @Test
  public void testAddEntry() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    String dn = "cn=New schema provider,cn=Schema Providers,cn=config";
    configHandler.addEntry(new LinkedHashMapEntry(dn));
    assertTrue(configHandler.hasEntry(DN.valueOf(dn)));
  }
  @Test(expectedExceptions=DirectoryException.class)
  public void testAddEntryExistingEntry() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    configHandler.addEntry(new LinkedHashMapEntry(DN_CORE_SCHEMA));
  }
  // TODO : disabled because fail when converting to server DN. Re-enable once migrated to SDK DN.
  @Test(enabled=false, expectedExceptions=DirectoryException.class)
  public void testAddEntryParentUnknown() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    configHandler.addEntry(new LinkedHashMapEntry("cn=Core Schema,cn=Schema Providers,cn=Providers,cn=config"));
  }
  @Test(expectedExceptions=DirectoryException.class)
  public void testAddEntryNoParent() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    configHandler.addEntry(new LinkedHashMapEntry(DN.rootDN()));
  }
  @Test
  public void testAddListenerWithAddEntry() throws Exception
  {
    String dn = "cn=New schema provider,cn=Schema Providers,cn=config";
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigAddListener listener = mock(ConfigAddListener.class);
    configHandler.registerAddListener(DN_SCHEMA_PROVIDERS, listener);
    when(listener.configAddIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
      thenReturn(true);
    when(listener.applyConfigurationAdd(any(Entry.class))).
      thenReturn(new ConfigChangeResult(ResultCode.SUCCESS, false));
    configHandler.addEntry(new LinkedHashMapEntry(dn));
    // ensure apply is called for listener
    verify(listener).applyConfigurationAdd(configHandler.getEntry(DN.valueOf(dn)));
  }
  @Test(expectedExceptions=DirectoryException.class)
  public void testAddListenerWithAddEntryWhenConfigNotAcceptable() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigAddListener listener = mock(ConfigAddListener.class);
    configHandler.registerAddListener(DN_SCHEMA_PROVIDERS, listener);
    when(listener.configAddIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
    thenReturn(false);
    configHandler.addEntry(new LinkedHashMapEntry("cn=New schema provider,cn=Schema Providers,cn=config"));
  }
  @Test(expectedExceptions=DirectoryException.class)
  public void testAddListenerWithAddEntryWhenFailureApplyingConfig() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigAddListener listener = mock(ConfigAddListener.class);
    configHandler.registerAddListener(DN_SCHEMA_PROVIDERS, listener);
    when(listener.configAddIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
      thenReturn(true);
    when(listener.applyConfigurationAdd(any(Entry.class))).
      thenReturn(new ConfigChangeResult(ResultCode.OTHER, false));
    configHandler.addEntry(new LinkedHashMapEntry("cn=New schema provider,cn=Schema Providers,cn=config"));
  }
  @Test
  public void testDeleteEntry() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    configHandler.deleteEntry(DN_CORE_SCHEMA);
    assertFalse(configHandler.hasEntry(DN_CORE_SCHEMA));
  }
  // TODO : disabled because fail when converting to server DN. Re-enable once migrated to SDK DN.
  @Test(enabled=false, expectedExceptions=DirectoryException.class)
  public void testDeleteEntryUnexistingEntry() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    configHandler.deleteEntry(DN.valueOf("cn=Unexisting provider,cn=Schema Providers,cn=config"));
  }
  @Test(enabled=false, expectedExceptions=DirectoryException.class)
  public void testDeleteEntryWithChildren() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    configHandler.deleteEntry(DN_SCHEMA_PROVIDERS);
  }
  // TODO : disabled because fail when converting to server DN. Re-enable once migrated to SDK DN.
  @Test(enabled=false, expectedExceptions=DirectoryException.class)
  public void testDeleteEntryUnknownParent() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    configHandler.deleteEntry(DN.valueOf("cn=Core Schema,cn=Schema Providers,cn=Providers,cn=config"));
  }
  @Test
  public void testDeleteListenerWithDeleteEntry() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigDeleteListener listener = mock(ConfigDeleteListener.class);
    configHandler.registerDeleteListener(DN_SCHEMA_PROVIDERS, listener);
    Entry entryToDelete = configHandler.getEntry(DN_CORE_SCHEMA);
    when(listener.configDeleteIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
      thenReturn(true);
    when(listener.applyConfigurationDelete(any(Entry.class))).
      thenReturn(new ConfigChangeResult(ResultCode.SUCCESS, false));
    configHandler.deleteEntry(DN_CORE_SCHEMA);
    // ensure apply is called for listener
    verify(listener).applyConfigurationDelete(entryToDelete);
  }
  @Test(expectedExceptions=DirectoryException.class)
  public void testDeleteListenerWithDeleteEntryWhenConfigNotAcceptable() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigDeleteListener listener = mock(ConfigDeleteListener.class);
    configHandler.registerDeleteListener(DN_SCHEMA_PROVIDERS, listener);
    when(listener.configDeleteIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
      thenReturn(false);
    configHandler.deleteEntry(DN_CORE_SCHEMA);
  }
  @Test(expectedExceptions=DirectoryException.class)
  public void testDeleteListenerWithDeleteEntryWhenFailureApplyingConfig() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigDeleteListener listener = mock(ConfigDeleteListener.class);
    configHandler.registerDeleteListener(DN_SCHEMA_PROVIDERS, listener);
    when(listener.configDeleteIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
      thenReturn(true);
    when(listener.applyConfigurationDelete(any(Entry.class))).
      thenReturn(new ConfigChangeResult(ResultCode.OTHER, false));
    configHandler.deleteEntry(DN_CORE_SCHEMA);
  }
  @Test
  public void testReplaceEntry() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    String dn = DN_CORE_SCHEMA.toString();
    configHandler.replaceEntry(
        new LinkedHashMapEntry("dn: " + dn, "objectclass: ds-cfg-schema-provider", "ds-cfg-enabled: true"),
        new LinkedHashMapEntry("dn: " + dn, "objectclass: ds-cfg-schema-provider", "ds-cfg-enabled: false"));
    assertTrue(configHandler.hasEntry(DN_CORE_SCHEMA));
    assertEquals(configHandler.getEntry(DN_CORE_SCHEMA).getAttribute("ds-cfg-enabled").firstValueAsString(), "false");
  }
  @Test
  public void testChangeListenerWithReplaceEntry() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigChangeListener listener = mock(ConfigChangeListener.class);
    configHandler.registerChangeListener(DN_CORE_SCHEMA, listener);
    when(listener.configChangeIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
      thenReturn(true);
    when(listener.applyConfigurationChange(any(Entry.class))).
      thenReturn(new ConfigChangeResult(ResultCode.SUCCESS, false));
    Entry oldEntry = configHandler.getEntry(DN_CORE_SCHEMA);
    configHandler.replaceEntry(oldEntry,
        new LinkedHashMapEntry("dn: " + DN_CORE_SCHEMA.toString(),
            "objectclass: ds-cfg-schema-provider",
            "ds-cfg-enabled: false"));
    // ensure apply is called for listener
    verify(listener).applyConfigurationChange(configHandler.getEntry(DN_CORE_SCHEMA));
  }
  @Test(expectedExceptions=DirectoryException.class)
  public void testChangeListenerWithReplaceEntryWhenConfigNotAcceptable() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigChangeListener listener = mock(ConfigChangeListener.class);
    configHandler.registerChangeListener(DN_CORE_SCHEMA, listener);
    when(listener.configChangeIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
      thenReturn(false);
    Entry oldEntry = configHandler.getEntry(DN_CORE_SCHEMA);
    configHandler.replaceEntry(oldEntry,
        new LinkedHashMapEntry("dn: " + DN_CORE_SCHEMA.toString(),
            "objectclass: ds-cfg-schema-provider",
            "ds-cfg-enabled: false"));
  }
  @Test(expectedExceptions=DirectoryException.class)
  public void testChangeListenerWithReplaceEntryWhenFailureApplyingConfig() throws Exception
  {
    ConfigurationHandler configHandler = getConfigurationHandler();
    ConfigChangeListener listener = mock(ConfigChangeListener.class);
    configHandler.registerChangeListener(DN_CORE_SCHEMA, listener);
    when(listener.configChangeIsAcceptable(any(Entry.class), any(LocalizableMessageBuilder.class))).
      thenReturn(true);
    when(listener.applyConfigurationChange(any(Entry.class))).
      thenReturn(new ConfigChangeResult(ResultCode.OTHER, false));
    Entry oldEntry = configHandler.getEntry(DN_CORE_SCHEMA);
    configHandler.replaceEntry(oldEntry,
        new LinkedHashMapEntry("dn: " + DN_CORE_SCHEMA.toString(),
            "objectclass: ds-cfg-schema-provider",
            "ds-cfg-enabled: false"));
  }
}
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/core/SchemaHandlerTestCase.java
New file
@@ -0,0 +1,96 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS.
 */
package org.opends.server.core;
import static org.assertj.core.api.Assertions.*;
import static org.opends.server.ServerContextBuilder.*;
import java.io.File;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.opends.server.TestCaseUtils;
import org.opends.server.schema.SchemaUpdater;
import org.opends.server.types.InitializationException;
import org.testng.annotations.Test;
@SuppressWarnings("javadoc")
public class SchemaHandlerTestCase extends CoreTestCase
{
  private static final String DIRECTORY_STRING_SYNTAX_OID = "1.3.6.1.4.1.1466.115.121.1.15";
  @Test
  public void testSchemaInitialization() throws Exception
  {
    MockSchemaUpdater schemaUpdater = new MockSchemaUpdater();
    initializeSchemaHandler(schemaUpdater);
    assertThat(schemaUpdater.getSchemaBuilder()).isNotNull();
    Schema schema = schemaUpdater.getSchemaBuilder().toSchema();
    assertThat(schema.getMatchingRules()).isNotEmpty(); // some matching rules defined
    schema.getSyntax(DIRECTORY_STRING_SYNTAX_OID);
    schema.getAttributeType("javaClassName"); // from file 03-rfc2713
    schema.getAttributeType("nisNetIdUser"); // from file 5-solaris.ldif
    schema.getObjectClass("changeLogEntry"); // from file 03-changelog.ldif
  }
  private void initializeSchemaHandler(SchemaUpdater updater) throws InitializationException, ConfigException
  {
    final ServerContext serverContext = aServerContext().
        setSchemaDirectory(new File(TestCaseUtils.getBuildRoot(), "resource/schema")).
        setConfigFile(TestCaseUtils.getTestResource("config-small.ldif")).
        setSchemaUpdater(updater).
        withConfigurationBootstrapped().
        build();
    SchemaHandler schemaHandler = new SchemaHandler();
    schemaHandler.initialize(serverContext);
  }
  private static final class MockSchemaUpdater implements SchemaUpdater
  {
    private SchemaBuilder schemaBuilder;
    @Override
    public boolean updateSchema(SchemaBuilder schemaBuilder)
    {
      this.schemaBuilder = schemaBuilder;
      return true;
    }
    @Override
    public SchemaBuilder getSchemaBuilder()
    {
      return schemaBuilder;
    }
  }
}
opendj-sdk/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/schema/CoreSchemaProviderTestCase.java
New file
@@ -0,0 +1,94 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2014 ForgeRock AS.
 */
package org.opends.server.schema;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.opends.server.TestCaseUtils.*;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.opendj.server.config.server.CoreSchemaCfg;
import org.opends.server.ConfigurationMock;
import org.opends.server.core.CoreTestCase;
import org.testng.annotations.Test;
@SuppressWarnings("javadoc")
public class CoreSchemaProviderTestCase extends CoreTestCase
{
  private static final String DIRECTORY_STRING_SYNTAX_OID = "1.3.6.1.4.1.1466.115.121.1.15";
  private static final String GENERALIZED_TIME_MATCHING_RULE_OID = "2.5.13.27";
  @Test
  public void testInitializeWithoutError() throws Exception
  {
    CoreSchemaCfg coreSchemaCfg = ConfigurationMock.mockCfg(CoreSchemaCfg.class);
    CoreSchemaProvider provider = new CoreSchemaProvider();
    provider.initialize(coreSchemaCfg, new SchemaBuilder(), mock(SchemaUpdater.class));
    verify(coreSchemaCfg).addCoreSchemaChangeListener(provider);
  }
  @Test
  public void testEnableZeroLengthDirectoryStrings() throws Exception
  {
    CoreSchemaCfg coreSchemaCfg = ConfigurationMock.mockCfg(CoreSchemaCfg.class);
    when(coreSchemaCfg.isAllowZeroLengthValuesDirectoryString()).thenReturn(true);
    SchemaBuilder schemaBuilder = new SchemaBuilder();
    CoreSchemaProvider provider = new CoreSchemaProvider();
    provider.initialize(coreSchemaCfg, schemaBuilder, mock(SchemaUpdater.class));
    assertThat(schemaBuilder.toSchema().allowZeroLengthDirectoryStrings()).isTrue();
  }
  @Test
  public void testDisableSyntax() throws Exception
  {
    CoreSchemaCfg coreSchemaCfg = ConfigurationMock.mockCfg(CoreSchemaCfg.class);
    when(coreSchemaCfg.getDisabledSyntax()).thenReturn(newSortedSet(DIRECTORY_STRING_SYNTAX_OID));
    SchemaBuilder schemaBuilder = new SchemaBuilder(Schema.getCoreSchema());
    CoreSchemaProvider provider = new CoreSchemaProvider();
    provider.initialize(coreSchemaCfg, schemaBuilder, mock(SchemaUpdater.class));
    assertThat(schemaBuilder.toSchema().hasSyntax(DIRECTORY_STRING_SYNTAX_OID)).isFalse();
  }
  @Test
  public void testDisableMatchingRule() throws Exception
  {
    CoreSchemaCfg coreSchemaCfg = ConfigurationMock.mockCfg(CoreSchemaCfg.class);
    when(coreSchemaCfg.getDisabledMatchingRule()).thenReturn(newSortedSet(GENERALIZED_TIME_MATCHING_RULE_OID));
    SchemaBuilder schemaBuilder = new SchemaBuilder(Schema.getCoreSchema());
    CoreSchemaProvider provider = new CoreSchemaProvider();
    provider.initialize(coreSchemaCfg, schemaBuilder, mock(SchemaUpdater.class));
    assertThat(schemaBuilder.toSchema().hasMatchingRule(DIRECTORY_STRING_SYNTAX_OID)).isFalse();
  }
}