From b2c8bccb09569add38f71fd1f1c8fc9d8e516712 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 25 Dec 2024 14:00:00 +0000
Subject: [PATCH] Store LDAPv3 database in SQL JDBC database (#454)

---
 opendj-server-legacy/src/test/java/org/opends/server/replication/GenerationIdTest.java                            |    3 
 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JDBCBackendConfiguration.xml |   74 ++++
 .github/workflows/deploy.yml                                                                                      |    3 
 opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/Storage.java                                   |  545 ++++++++++++++++++++++++++++++++++
 opendj-server-legacy/resource/schema/02-config.ldif                                                               |    8 
 pom.xml                                                                                                           |   16 -
 README.md                                                                                                         |    2 
 opendj-server-legacy/pom.xml                                                                                      |   31 -
 opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java                                  |   79 ++++
 .github/workflows/release.yml                                                                                     |    3 
 .github/workflows/build.yml                                                                                       |   38 +
 opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/EncryptedTestCase.java                         |   81 +++++
 opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/package-info.java                              |   18 +
 opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/Backend.java                                   |   31 +
 14 files changed, 881 insertions(+), 51 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c88f414..dec5178 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,8 +24,9 @@
         sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -c -s)/winehq-$(lsb_release -c -s).sources
         sudo apt-get update
         sudo apt install --install-recommends winehq-stable || sudo apt install --install-recommends winehq-staging
-        sudo mkdir -p /opt/wine/mono && sudo wget "https://dl.winehq.org/wine/wine-mono/8.0.0/wine-mono-8.0.0-x86.tar.xz" -P /opt/wine/mono && sudo tar -xf /opt/wine/mono/wine-mono-8.0.0-x86.tar.xz -C /opt/wine/mono && sudo rm /opt/wine/mono/wine-mono-8.0.0-x86.tar.xz
         wine --version
+        version="9.4.0"; sudo wget "https://dl.winehq.org/wine/wine-mono/$version/wine-mono-$version-x86.msi" -O /tmp/wine-mono.msi
+        wine msiexec /i /tmp/wine-mono.msi
     - uses: actions/checkout@v4
       with:
         fetch-depth: 0
@@ -41,12 +42,7 @@
          path: ~/.m2/repository
          key: ${{ runner.os }}-m2-repository-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-m2-repository
-    - name: Run docker cassandra
-      if: runner.os == 'Linux'
-      run:   |
-        docker run --rm -it -d -p 9042:9042 --name cassandra cassandra
-        timeout 5m bash -c 'until docker logs cassandra | grep -q "Created default superuser role"; do sleep 5; done'
-    - name: Set Integration Test Environment 
+    - name: Set Integration Test Environment
       id: failsafe
       if: runner.os != 'Windows'
       run:   |
@@ -71,17 +67,37 @@
         opendj-server-legacy/target/package/opendj/bin/start-ds
         opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example2,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000
         opendj-server-legacy/target/package/opendj/bin/stop-ds
-        rm -rf opendj-server-legacy/target/package/opendj/config opendj-server-legacy/target/package/opendj/db opendj-server-legacy/target/package/opendj/changelogDb opendj-server-legacy/target/package/opendj/logs
+        rm -rf opendj-server-legacy/target/package/opendj/{config,db,changelogDb,logs}
     - name: Test LDAP in Cassandra
       if: runner.os == 'Linux'
       run:   |
+        docker run --rm -it -d -p 9042:9042 --name cassandra cassandra
+        timeout 5m bash -c 'until docker logs cassandra | grep -q "Created default superuser role"; do sleep 5; done'
         export OPENDJ_JAVA_ARGS="-server -Xmx512m -Ddatastax-java-driver.basic.contact-points.0=localhost:9042 -Ddatastax-java-driver.basic.load-balancing-policy.local-datacenter=datacenter1"
-        opendj-server-legacy/target/package/opendj/setup --backendType cas -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --sampleData 5000 --cli --acceptLicense --no-prompt
+        opendj-server-legacy/target/package/opendj/setup -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --cli --acceptLicense --no-prompt
+        opendj-server-legacy/target/package/opendj/bin/dsconfig create-backend -h localhost -p 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=userRoot --type cas --set base-dn:dc=example,dc=com --set db-directory:keyspace_name --set enabled:true --no-prompt --trustAll
+        opendj-server-legacy/target/package/opendj/bin/makeldif -o /tmp/test.ldif -c suffix=dc=example,dc=com opendj-server-legacy/target/package/opendj/config/MakeLDIF/example.template
+        opendj-server-legacy/target/package/opendj/bin/import-ldif --ldifFile /tmp/test.ldif --backendID=userRoot -h localhost -p 4444 --bindDN "cn=Directory Manager" --bindPassword password --trustAll
         opendj-server-legacy/target/package/opendj/bin/status --bindDN "cn=Directory Manager" --bindPassword password
         opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
-        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 5000
+        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000
         opendj-server-legacy/target/package/opendj/bin/stop-ds
-        rm -rf opendj-server-legacy/target/package/opendj/config opendj-server-legacy/target/package/opendj/db opendj-server-legacy/target/package/opendj/changelogDb opendj-server-legacy/target/package/opendj/logs
+        rm -rf opendj-server-legacy/target/package/opendj/{config,db,changelogDb,logs}
+    - name: Test LDAP in Postgres
+      if: runner.os == 'Linux'
+      run:   |
+        docker run --rm -it -d -p 5432:5432 -e POSTGRES_DB=database_name -e POSTGRES_PASSWORD=password --name postgres postgres
+        timeout 5m bash -c 'until docker logs postgres | grep -q "database system is ready to accept connections"; do sleep 5; done'
+        export OPENDJ_JAVA_ARGS="-server -Xmx512m"
+        opendj-server-legacy/target/package/opendj/setup -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --cli --acceptLicense --no-prompt
+        opendj-server-legacy/target/package/opendj/bin/dsconfig create-backend -h localhost -p 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=userRoot --type jdbc --set base-dn:dc=example,dc=com --set db-directory:jdbc:postgresql://localhost:5432/database_name?user=postgres\&password=password --set enabled:true --no-prompt --trustAll
+        opendj-server-legacy/target/package/opendj/bin/makeldif -o /tmp/test.ldif -c suffix=dc=example,dc=com opendj-server-legacy/target/package/opendj/config/MakeLDIF/example.template
+        opendj-server-legacy/target/package/opendj/bin/import-ldif --ldifFile /tmp/test.ldif --backendID=userRoot -h localhost -p 4444 --bindDN "cn=Directory Manager" --bindPassword password --trustAll
+        opendj-server-legacy/target/package/opendj/bin/status --bindDN "cn=Directory Manager" --bindPassword password
+        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
+        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000
+        opendj-server-legacy/target/package/opendj/bin/stop-ds
+        rm -rf opendj-server-legacy/target/package/opendj/{config,db,changelogDb,logs}
     - name: Test on Windows
       if: runner.os == 'Windows'
       run:   |
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 6abb4e4..d5caea8 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -24,8 +24,9 @@
           sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -c -s)/winehq-$(lsb_release -c -s).sources
           sudo apt-get update
           sudo apt install --install-recommends winehq-stable || sudo apt install --install-recommends winehq-staging
-          sudo mkdir -p /opt/wine/mono && sudo wget "https://dl.winehq.org/wine/wine-mono/8.0.0/wine-mono-8.0.0-x86.tar.xz" -P /opt/wine/mono && sudo tar -xf /opt/wine/mono/wine-mono-8.0.0-x86.tar.xz -C /opt/wine/mono && sudo rm /opt/wine/mono/wine-mono-8.0.0-x86.tar.xz
           wine --version
+          version="9.4.0"; sudo wget "https://dl.winehq.org/wine/wine-mono/$version/wine-mono-$version-x86.msi" -O /tmp/wine-mono.msi
+          wine msiexec /i /tmp/wine-mono.msi
       - uses: actions/checkout@v4
         with:
           fetch-depth: 0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 034e8ae..f1d9e94 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -28,8 +28,9 @@
           sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -c -s)/winehq-$(lsb_release -c -s).sources
           sudo apt-get update
           sudo apt install --install-recommends winehq-stable || sudo apt install --install-recommends winehq-staging
-          sudo mkdir -p /opt/wine/mono && sudo wget "https://dl.winehq.org/wine/wine-mono/8.0.0/wine-mono-8.0.0-x86.tar.xz" -P /opt/wine/mono && sudo tar -xf /opt/wine/mono/wine-mono-8.0.0-x86.tar.xz -C /opt/wine/mono && sudo rm /opt/wine/mono/wine-mono-8.0.0-x86.tar.xz
           wine --version
+          version="9.4.0"; sudo wget "https://dl.winehq.org/wine/wine-mono/$version/wine-mono-$version-x86.msi" -O /tmp/wine-mono.msi
+          wine msiexec /i /tmp/wine-mono.msi
       - uses: actions/checkout@v4
         with:
           fetch-depth: 0
diff --git a/README.md b/README.md
index 410a6cd..d017252 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
 OpenDJ is an [LDAPv3](http://tools.ietf.org/html/rfc4510) compliant directory service, which has been developed 
 for the Java platform, providing a high performance, highly available, and secure store for the identities managed 
 by your organization. Its easy installation process, combined with the power of the Java platform makes OpenDJ
-the simplest, fastest directory to deploy and manage and allow [store LDAPv3 database in Cassandra/Scylla cluster](https://github.com/OpenIdentityPlatform/OpenDJ/wiki/How-To#store-ldap-catalog-data-in-cassandra-nosql-cluster).
+the simplest, fastest directory to deploy and manage and allow store LDAPv3 database in [SQL JDBC database](https://github.com/OpenIdentityPlatform/OpenDJ/wiki/How-To#store-ldap-catalog-data-in-jdbc-databse) or [NoSQL Cassandra/Scylla cluster](https://github.com/OpenIdentityPlatform/OpenDJ/wiki/How-To#store-ldap-catalog-data-in-cassandra-nosql-cluster).
 
 An open source, lightweight, embeddable directory that can easily share real-time customer, device, and user identity data across enterprise, cloud, social, and mobile environments.
 * Massive data scale and high availability provide developers with ultra-lightweight ways to access identity data
diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JDBCBackendConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JDBCBackendConfiguration.xml
new file mode 100644
index 0000000..4f832d2
--- /dev/null
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JDBCBackendConfiguration.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2024 3A Systems LLC
+  ! -->
+<adm:managed-object name="jdbc-backend" plural-name="jdbc-backends"
+  package="org.forgerock.opendj.server.config"
+  extends="pluggable-backend" xmlns:adm="http://opendj.forgerock.org/admin"
+  xmlns:ldap="http://opendj.forgerock.org/admin-ldap"
+  xmlns:cli="http://opendj.forgerock.org/admin-cli">
+  <adm:synopsis>
+    A <adm:user-friendly-name/> stores application data in JDBC source.
+  </adm:synopsis>
+  <adm:description>
+    It is the traditional "directory server" backend and is similar to
+    the backends provided by the Sun Java System Directory Server. The
+    <adm:user-friendly-name />
+    stores the entries in an encoded form and also provides indexes that
+    can be used to quickly locate target entries based on different
+    kinds of criteria.
+  </adm:description>
+  <adm:profile name="ldap">
+    <ldap:object-class>
+      <ldap:name>ds-cfg-jdbc-backend</ldap:name>
+      <ldap:superior>ds-cfg-pluggable-backend</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.backends.jdbc.Backend
+        </adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+  </adm:property-override>
+  
+  <adm:property name="db-directory" mandatory="true">
+    <adm:TODO>Default this to the db/backend-id</adm:TODO>
+    <adm:synopsis>
+      Specifies the connection string
+    </adm:synopsis>
+    <adm:description>
+      jdbc:postgresql://localhost/test
+    </adm:description>
+    <adm:requires-admin-action>
+      <adm:component-restart />
+    </adm:requires-admin-action>
+    <adm:default-behavior>
+      <adm:defined>
+        <adm:value>jdbc:postgresql://localhost/test</adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:string />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:name>ds-cfg-db-directory</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+</adm:managed-object>
diff --git a/opendj-server-legacy/pom.xml b/opendj-server-legacy/pom.xml
index 97008f7..8d75ab8 100644
--- a/opendj-server-legacy/pom.xml
+++ b/opendj-server-legacy/pom.xml
@@ -259,6 +259,11 @@
 	    </exclusions>
     </dependency>
     <dependency>
+      <groupId>org.postgresql</groupId>
+      <artifactId>postgresql</artifactId>
+      <version>42.7.4</version>
+    </dependency>
+    <dependency>
       <groupId>org.testng</groupId>
       <artifactId>testng</artifactId>
       <scope>test</scope>
@@ -267,7 +272,13 @@
     <dependency>
       <groupId>org.testcontainers</groupId>
       <artifactId>cassandra</artifactId>
-      <version>1.19.0</version>
+      <version>1.20.4</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>postgresql</artifactId>
+      <version>1.20.4</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
@@ -1082,24 +1093,6 @@
         </executions>
       </plugin>
 
-      <!-- Build javadoc -->
-      <plugin>
-        <groupId>org.openidentityplatform.maven.plugins</groupId>
-        <artifactId>javadoc-updater-maven-plugin</artifactId>
-        <version>1.0.0</version>
-        <executions>
-          <execution>
-            <phase>site</phase>
-            <goals>
-              <goal>fixjavadoc</goal>
-            </goals>
-            <configuration>
-              <directory>${project.build.directory}/site/javadoc</directory>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-
       <!-- Release project -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
diff --git a/opendj-server-legacy/resource/schema/02-config.ldif b/opendj-server-legacy/resource/schema/02-config.ldif
index 71bb719..697a1ae 100644
--- a/opendj-server-legacy/resource/schema/02-config.ldif
+++ b/opendj-server-legacy/resource/schema/02-config.ldif
@@ -15,7 +15,7 @@
 # Portions Copyright 2011 profiq, s.r.o.
 # Portions Copyright 2012 Manuel Gaupp
 # Portions copyright 2015 Edan Idzerda
-# Portions copyright 2023 3A Systems LLC
+# Portions copyright 2023-2024 3A Systems LLC
 
 # This file contains the attribute type and objectclass definitions for use
 # with the Directory Server configuration.
@@ -6013,6 +6013,12 @@
   STRUCTURAL
   MUST ds-cfg-db-directory
   X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.60142.2.1.2.2
+  NAME 'ds-cfg-jdbc-backend'
+  SUP ds-cfg-pluggable-backend
+  STRUCTURAL
+  MUST ds-cfg-db-directory
+  X-ORIGIN 'OpenDJ Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.27
   NAME 'ds-task-reset-change-number'
   SUP ds-task
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/Backend.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/Backend.java
new file mode 100644
index 0000000..ce3f3f4
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/Backend.java
@@ -0,0 +1,31 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2024 3A Systems, LLC.
+ */
+package org.opends.server.backends.jdbc;
+
+import org.forgerock.opendj.config.server.ConfigException;
+import org.forgerock.opendj.server.config.server.JDBCBackendCfg;
+import org.opends.server.backends.pluggable.BackendImpl;
+import org.opends.server.core.ServerContext;
+
+public class Backend extends BackendImpl<JDBCBackendCfg>{
+	
+	  @Override
+	  protected Storage configureStorage(JDBCBackendCfg cfg, ServerContext serverContext) throws ConfigException
+	  {
+	    return new Storage(cfg, serverContext);
+	  }
+
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/Storage.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/Storage.java
new file mode 100644
index 0000000..67b6959
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/Storage.java
@@ -0,0 +1,545 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2024 3A Systems, LLC.
+ */
+package org.opends.server.backends.jdbc;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.config.server.ConfigChangeResult;
+import org.forgerock.opendj.config.server.ConfigException;
+import org.forgerock.opendj.config.server.ConfigurationChangeListener;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.server.config.server.JDBCBackendCfg;
+import org.opends.server.backends.pluggable.spi.*;
+import org.opends.server.core.ServerContext;
+import org.opends.server.types.BackupConfig;
+import org.opends.server.types.BackupDirectory;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.RestoreConfig;
+import org.opends.server.util.BackupManager;
+
+import java.sql.*;
+import java.util.*;
+
+import static org.opends.server.backends.pluggable.spi.StorageUtils.addErrorMessage;
+import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
+
+public class Storage implements org.opends.server.backends.pluggable.spi.Storage, ConfigurationChangeListener<JDBCBackendCfg>{
+	
+	private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private JDBCBackendCfg config;
+	  
+	public Storage(JDBCBackendCfg cfg, ServerContext serverContext) {
+        this.config = cfg;
+	    cfg.addJDBCChangeListener(this);
+	}
+
+	//config
+	@Override
+	public boolean isConfigurationChangeAcceptable(JDBCBackendCfg configuration,List<LocalizableMessage> unacceptableReasons) {
+		return true;
+	}
+
+	@Override
+	public ConfigChangeResult applyConfigurationChange(JDBCBackendCfg cfg) {
+		final ConfigChangeResult ccr = new ConfigChangeResult();
+	    try
+	    {
+	    	this.config = cfg;
+	    }
+	    catch (Exception e)
+	    {
+	      addErrorMessage(ccr, LocalizableMessage.raw(stackTraceToSingleLineString(e)));
+	    }
+	    return ccr;
+	}
+
+	ResultSet executeResultSet(PreparedStatement statement) throws SQLException {
+		if (logger.isTraceEnabled()) {
+			logger.trace(LocalizableMessage.raw("jdbc: %s",statement));
+		}
+		return statement.executeQuery();
+	}
+
+	boolean execute(PreparedStatement statement) throws SQLException {
+		if (logger.isTraceEnabled()) {
+			logger.trace(LocalizableMessage.raw("jdbc: %s",statement));
+		}
+		return statement.execute();
+	}
+
+	Connection con;
+	@Override
+	public void open(AccessMode accessMode) throws Exception {
+		con=DriverManager.getConnection(config.getDBDirectory());
+		con.setAutoCommit(false);
+		con.setReadOnly(!AccessMode.READ_WRITE.equals(accessMode));
+		storageStatus = StorageStatus.working();
+	}
+
+	private StorageStatus storageStatus = StorageStatus.lockedDown(LocalizableMessage.raw("closed"));
+	@Override
+	public StorageStatus getStorageStatus() {
+		return storageStatus;
+	}
+	
+	@Override
+	public void close() {
+		storageStatus = StorageStatus.lockedDown(LocalizableMessage.raw("closed"));
+		try {
+			if (con != null && !con.isClosed()) {
+				con.close();
+			}
+		} catch (SQLException e) {
+			logger.error(LocalizableMessage.raw("close(): %s",e),e);
+		}
+		con=null;
+	}
+
+	String getTableName(TreeName treeName) {
+		return "\"OpenDJ"+treeName.toString()+"\"";
+	}
+
+	@Override
+	public void removeStorageFiles() throws StorageRuntimeException {
+		final boolean isOpen=getStorageStatus().isWorking();
+		if (!isOpen) {
+			try {
+				open(AccessMode.READ_WRITE);
+			}catch (Exception e) {
+				throw new StorageRuntimeException(e);
+			}
+		}
+		try {
+			for (TreeName treeName : listTrees()) {
+				final PreparedStatement statement=con.prepareStatement("drop table "+getTableName(treeName));
+				execute(statement);
+			}
+		}catch (Throwable e) {
+			throw new StorageRuntimeException(e);
+		}
+		if (!isOpen) {
+			close();
+		}
+	}
+	
+	//operation
+	@Override
+	public <T> T read(ReadOperation<T> readOperation) throws Exception {
+		return readOperation.run(new ReadableTransactionImpl());
+	}
+
+	@Override
+	public void write(WriteOperation writeOperation) throws Exception {
+		try {
+			writeOperation.run(new WriteableTransactionTransactionImpl());
+			con.commit();
+		} catch (Exception e) {
+			try {
+				con.rollback();
+			} catch (SQLException ex) {}
+			throw e;
+		}
+	}
+
+	private class ReadableTransactionImpl implements ReadableTransaction {
+		@Override
+		public ByteString read(TreeName treeName, ByteSequence key) {
+			try {
+				final PreparedStatement statement=con.prepareStatement("select v from "+getTableName(treeName)+" where k=?");
+				statement.setBytes(1,key.toByteArray());
+				try(ResultSet rc=executeResultSet(statement)) {
+					return rc.next() ? ByteString.wrap(rc.getBytes("v")) : null;
+				}
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public Cursor<ByteString, ByteString> openCursor(TreeName treeName) {
+			return new CursorImpl(treeName);
+		}
+
+		@Override
+		public long getRecordCount(TreeName treeName) {
+			try {
+				final PreparedStatement statement=con.prepareStatement("select count(*) from "+getTableName(treeName));
+				try(ResultSet rc=executeResultSet(statement)) {
+					return rc.next() ? rc.getLong(1) : 0;
+				}
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+	}
+	private final class WriteableTransactionTransactionImpl extends ReadableTransactionImpl implements WriteableTransaction {
+		
+		public WriteableTransactionTransactionImpl() {
+			super();
+			try {
+				if (con.isReadOnly()) {
+					throw new ReadOnlyStorageException();
+				}
+			} catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public void openTree(TreeName treeName, boolean createOnDemand) {
+			if (createOnDemand) {
+				try {
+					final PreparedStatement statement=con.prepareStatement("create table if not exists "+getTableName(treeName)+" (k bytea primary key,v bytea)");
+					execute(statement);
+				}catch (SQLException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		}
+		
+		public void clearTree(TreeName treeName) {
+			try {
+				final PreparedStatement statement=con.prepareStatement("truncate table "+getTableName(treeName));
+				execute(statement);
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public void deleteTree(TreeName treeName) {
+			try {
+				final PreparedStatement statement=con.prepareStatement("drop table "+getTableName(treeName));
+				execute(statement);
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public void put(TreeName treeName, ByteSequence key, ByteSequence value) {
+			try {
+				delete(treeName,key);
+				final PreparedStatement statement=con.prepareStatement("insert into "+getTableName(treeName)+" (k,v) values(?,?) ");
+				statement.setBytes(1,key.toByteArray());
+				statement.setBytes(2,value.toByteArray());
+				execute(statement);
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public boolean update(TreeName treeName, ByteSequence key, UpdateFunction f) {
+			final ByteString oldValue=read(treeName,key);
+			final ByteSequence newValue=f.computeNewValue(oldValue);
+			if (Objects.equals(newValue, oldValue))
+	        {
+				return false;
+	        }
+	        if (newValue == null)
+	        {
+	        	delete(treeName, key);
+	        	return true;
+	        }
+	        put(treeName,key,newValue);
+			return true;
+		}
+
+		@Override
+		public boolean delete(TreeName treeName, ByteSequence key) {
+			try {
+				final PreparedStatement statement=con.prepareStatement("delete from "+getTableName(treeName)+" where k=?");
+				statement.setBytes(1,key.toByteArray());
+				execute(statement);
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+			return true;
+		}
+	}
+	
+	private final class CursorImpl implements Cursor<ByteString, ByteString> {
+		final TreeName treeName;
+		//final WriteableTransactionTransactionImpl tx;
+
+		ResultSet rc;
+
+		public CursorImpl(TreeName treeName) {
+			this.treeName=treeName;
+			//this.tx=tx;
+			try {
+				final PreparedStatement statement=con.prepareStatement("select k,v from "+getTableName(treeName)+" order by k",
+						ResultSet.TYPE_SCROLL_SENSITIVE,
+						ResultSet.CONCUR_UPDATABLE);
+				rc=executeResultSet(statement);
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public boolean next() {
+			try {
+				return rc.next();
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public boolean isDefined() {
+			try{
+				return rc.getRow()>0;
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public ByteString getKey() throws NoSuchElementException {
+			if (!isDefined()) {
+				throw new NoSuchElementException();
+			}
+			try{
+				return ByteString.wrap(rc.getBytes("k"));
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public ByteString getValue() throws NoSuchElementException {
+			if (!isDefined()) {
+				throw new NoSuchElementException();
+			}
+			try{
+				return ByteString.wrap(rc.getBytes("v"));
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public void delete() throws NoSuchElementException, UnsupportedOperationException {
+			if (!isDefined()) {
+				throw new NoSuchElementException();
+			}
+			try{
+				rc.deleteRow();
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public void close() {
+			if (rc!=null) {
+				try{
+					rc.close();
+				}catch (SQLException e) {
+					throw new RuntimeException(e);
+				}
+				rc = null;
+			}
+		}
+
+
+		@Override
+		public boolean positionToKeyOrNext(ByteSequence key) {
+			if (!isDefined() || key.compareTo(getKey())<0) { //restart iterator
+				try{
+					rc.first();
+				}catch (SQLException e) {
+					throw new RuntimeException(e);
+				}
+			}
+			try{
+				if (!isDefined()){
+					return false;
+				}
+				do {
+					if (key.compareTo(getKey())<=0) {
+						return true;
+					}
+				}while(rc.next());
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+			return false;
+		}
+		
+		@Override
+		public boolean positionToKey(ByteSequence key) {
+			if (!isDefined() || key.compareTo(getKey())<0) {  //restart iterator
+				try{
+					rc.first();
+				}catch (SQLException e) {
+					throw new RuntimeException(e);
+				}
+			}
+			if (!isDefined()){
+				return false;
+			}
+			if (isDefined() && key.compareTo(getKey())==0) {
+				return true;
+			}
+			try{
+				do {
+					if (key.compareTo(getKey())==0) {
+						return true;
+					}
+				}while(rc.next());
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+			return false;
+		}
+
+		
+		@Override
+		public boolean positionToLastKey() {
+			try{
+				return rc.last();
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		@Override
+		public boolean positionToIndex(int index) {
+			try{
+				rc.first();
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+			if (!isDefined()){
+				return false;
+			}
+			int ct=0;
+			try{
+				do {
+					if (ct==index) {
+						return true;
+					}
+					ct++;
+				}while(rc.next());
+			}catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+			return false;
+		}
+	}
+	
+	@Override
+	public Set<TreeName> listTrees() {
+		final Set<TreeName> res=new HashSet<>();
+		try(ResultSet rs = con.getMetaData().getTables(null, null, "OpenDJ%", new String[]{"TABLE"})) {
+			while (rs.next()) {
+				res.add(TreeName.valueOf(rs.getString("TABLE_NAME").substring(6)));
+			}
+		} catch (SQLException e) {
+			throw new RuntimeException(e);
+		}
+		return res;
+	}
+	
+
+	private final class ImporterImpl implements Importer {
+		final WriteableTransactionTransactionImpl tx;
+		
+		final Boolean isOpen;
+		
+		public ImporterImpl() {
+			isOpen=getStorageStatus().isWorking();
+			if (!isOpen) {
+				try {
+					open(AccessMode.READ_WRITE);
+				}catch (Exception e) {
+					throw new StorageRuntimeException(e);
+				}
+			}
+			tx=new WriteableTransactionTransactionImpl();
+		}
+		
+		@Override
+		public void close() {
+			try {
+				con.commit();
+			} catch (SQLException e) {
+				throw new RuntimeException(e);
+			}
+			if (!isOpen) {
+				Storage.this.close();
+			}
+		}
+		
+		@Override
+		public void clearTree(TreeName name) {
+			tx.clearTree(name);
+		}
+		
+		@Override
+		public void put(TreeName treeName, ByteSequence key, ByteSequence value) {
+			tx.put(treeName, key, value);
+		}
+		
+		@Override
+		public ByteString read(TreeName treeName, ByteSequence key) {
+			return tx.read(treeName, key);
+		}
+		
+		@Override
+		public SequentialCursor<ByteString, ByteString> openCursor(TreeName treeName) {
+			return tx.openCursor(treeName);
+		}
+	}
+	
+	//import
+	@Override
+	public Importer startImport() throws ConfigException, StorageRuntimeException {
+		return new ImporterImpl();
+	}
+	
+	//backup
+	@Override
+	public boolean supportsBackupAndRestore() {
+		return true;
+	}
+
+	@Override
+	public void createBackup(BackupConfig backupConfig) throws DirectoryException
+	{
+		// TODO backup over snapshot or SQL export
+		//new BackupManager(config.getBackendId()).createBackup(this, backupConfig);
+	}
+
+	@Override
+	public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
+	{
+		new BackupManager(config.getBackendId()).removeBackup(backupDirectory, backupID);
+	}
+
+	@Override
+	public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
+	{
+		// TODO restore over snapshot or SQL export
+		//new BackupManager(config.getBackendId()).restoreBackup(this, restoreConfig);
+	}
+
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/package-info.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/package-info.java
new file mode 100644
index 0000000..d29cef4
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2024 3A Systems LLC.
+ */
+@org.opends.server.types.PublicAPI(
+     stability=org.opends.server.types.StabilityLevel.PRIVATE)
+package org.opends.server.backends.jdbc;
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/EncryptedTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/EncryptedTestCase.java
new file mode 100644
index 0000000..6b0279c
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/EncryptedTestCase.java
@@ -0,0 +1,81 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2023-2024 3A Systems, LLC.
+ */
+package org.opends.server.backends.jdbc;
+
+import org.forgerock.opendj.server.config.server.JDBCBackendCfg;
+import org.opends.server.backends.pluggable.PluggableBackendImplTestCase;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testng.SkipException;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+
+import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
+import static org.mockito.Mockito.when;
+
+@Test
+public class EncryptedTestCase extends PluggableBackendImplTestCase<JDBCBackendCfg> {
+	PostgreSQLContainer container;
+
+	@BeforeClass
+	@Override
+	public void setUp() throws Exception {
+		if(DockerClientFactory.instance().isDockerAvailable()) {
+			container = new PostgreSQLContainer<>("postgres:latest")
+					.withExposedPorts(5432)
+					.withUsername("postgres")
+					.withPassword("password")
+					.withDatabaseName("database_name");
+			container.start();
+		}
+		try(Connection con= DriverManager.getConnection(createBackendCfg().getDBDirectory())){
+		} catch (Exception e) {
+			throw new SkipException("run before test: docker run --rm -it -p 5432:5432 -e POSTGRES_DB=database_name -e POSTGRES_PASSWORD=password --name postgres postgres");
+		}
+		super.setUp();
+	}
+
+	@Override
+	protected Backend createBackend() {
+		return new Backend();
+	}
+
+	@Override
+	protected JDBCBackendCfg createBackendCfg() {
+		JDBCBackendCfg backendCfg = mockCfg(JDBCBackendCfg.class);
+		when(backendCfg.getBackendId()).thenReturn("EncPsqlTestCase"+System.currentTimeMillis());
+		when(backendCfg.getDBDirectory()).thenReturn("jdbc:postgresql://localhost:"+ ((container==null)?"5432":container.getMappedPort(5432))+"/database_name?user=postgres&password=password");
+
+		when(backendCfg.isConfidentialityEnabled()).thenReturn(true);
+		when(backendCfg.getCipherKeyLength()).thenReturn(128);
+		when(backendCfg.getCipherTransformation()).thenReturn("AES/CBC/PKCS5Padding");
+		return backendCfg;
+	}
+
+
+	@AfterClass
+	@Override
+	public void cleanUp() throws Exception {
+		super.cleanUp();
+		if(container != null) {
+			container.close();
+		}
+	}
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java
new file mode 100644
index 0000000..0b01008
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java
@@ -0,0 +1,79 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2024 3A Systems, LLC.
+ */
+package org.opends.server.backends.jdbc;
+
+import org.forgerock.opendj.server.config.server.JDBCBackendCfg;
+import org.opends.server.backends.pluggable.PluggableBackendImplTestCase;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testng.SkipException;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+
+import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
+import static org.mockito.Mockito.when;
+
+//docker run --rm -it -p 5432:5432 -e POSTGRES_PASSWORD=password --name postgres postgres
+
+@Test
+public class TestCase extends PluggableBackendImplTestCase<JDBCBackendCfg> {
+
+	PostgreSQLContainer container;
+
+	@BeforeClass
+	@Override
+	public void setUp() throws Exception {
+		if(DockerClientFactory.instance().isDockerAvailable()) {
+			container = new PostgreSQLContainer<>("postgres:latest")
+					.withExposedPorts(5432)
+					.withUsername("postgres")
+					.withPassword("password")
+					.withDatabaseName("database_name");
+			container.start();
+		}
+		try(Connection con= DriverManager.getConnection(createBackendCfg().getDBDirectory())){
+		} catch (Exception e) {
+			throw new SkipException("run before test: docker run --rm -it -p 5432:5432 -e POSTGRES_DB=database_name -e POSTGRES_PASSWORD=password --name postgres postgres");
+		}
+		super.setUp();
+	}
+
+	@Override
+	protected Backend createBackend() {
+		return new Backend();
+	}
+
+	@Override
+	protected JDBCBackendCfg createBackendCfg() {
+		JDBCBackendCfg backendCfg = mockCfg(JDBCBackendCfg.class);
+		when(backendCfg.getBackendId()).thenReturn("PsqlTestCase");
+		when(backendCfg.getDBDirectory()).thenReturn("jdbc:postgresql://localhost:"+ ((container==null)?"5432":container.getMappedPort(5432))+"/database_name?user=postgres&password=password");
+		return backendCfg;
+	}
+
+	@AfterClass
+	@Override
+	public void cleanUp() throws Exception {
+		super.cleanUp();
+		if(container != null) {
+			container.close();
+		}
+	}
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/GenerationIdTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/GenerationIdTest.java
index b5fa909..2d1c828 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/replication/GenerationIdTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/GenerationIdTest.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2010 Sun Microsystems, Inc.
  * Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2023-2024 3A Systems, LLC.
  */
 package org.opends.server.replication;
 
@@ -990,7 +991,7 @@
   private void waitForStableGenerationId(final long expectedGenId) throws Exception
   {
     TestTimer timer = new TestTimer.Builder()
-      .maxSleep(20, SECONDS)
+      .maxSleep(30, SECONDS)
       .sleepTimes(100, MILLISECONDS)
       .toTimer();
     timer.repeatUntilSuccess(new CallableVoid()
diff --git a/pom.xml b/pom.xml
index a7b14ef..b529fee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -286,22 +286,6 @@
     	<finalName>${project.groupId}.${project.artifactId}</finalName>
         <plugins>
              <plugin>
-                <groupId>org.openidentityplatform.maven.plugins</groupId>
-                <artifactId>javadoc-updater-maven-plugin</artifactId>
-                <version>1.0.0</version>
-                <executions>
-                    <execution>
-                        <phase>site</phase>
-                        <goals>
-                            <goal>fixjavadoc</goal>
-                        </goals>
-                        <configuration>
-                            <directory>${project.reporting.outputDirectory}</directory>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-source-plugin</artifactId>
                 <executions>

--
Gitblit v1.10.0