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

vharseko
22.30.2023 7bc10f9671b187cecc2be8f5c52f7b4a7272f9f0
Allow store LDAP catalog data in CASSANDRA noSQL cluster --backendType cas (ldapv3 to cassandra) (#302)

export OPENDJ_JAVA_ARGS="-server -Xmx1g -Ddatastax-java-driver.basic.contact-points.0=localhost:9042 -Ddatastax-java-driver.basic.load-balancing-policy.local-datacenter=datacenter1"

./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
6 files added
12 files modified
864 ■■■■■ changed files
.github/workflows/build.yml 36 ●●●● patch | view | raw | blame | history
opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransport.java 1 ●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/stylesheets/abbreviations.xsl 3 ●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CASBackendConfiguration.xml 79 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/pom.xml 14 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/schema/02-config.ldif 7 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java 12 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/Backend.java 16 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/Storage.java 521 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/package-info.java 18 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/OnDiskMergeImporter.java 1 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/tools/BackendTypeHelper.java 6 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java 16 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/TestListener.java 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/cassandra/EncryptedTestCase.java 62 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/cassandra/TestCase.java 56 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java 12 ●●●●● patch | view | raw | blame | history
pom.xml 2 ●●● patch | view | raw | blame | history
.github/workflows/build.yml
@@ -41,31 +41,49 @@
         path: ~/.m2/repository
         key: ${{ runner.os }}-m2-repository-${{ hashFiles('**/pom.xml') }}
         restore-keys: ${{ runner.os }}-m2-repository
    - name: Set Integration Test Environment
      id: maven-profile-flag
    - 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
      id: failsafe
      if: runner.os != 'Windows'
      run:   |
        echo "MAVEN_PROFILE_FLAG=-P precommit" >> $GITHUB_OUTPUT
      run:   |
        echo "MAVEN_PROFILE_FLAG=-P precommit" >> $GITHUB_OUTPUT
    - name: Build with Maven
      timeout-minutes: 180
      env:
        MAVEN_OPTS: -Dhttps.protocols=TLSv1.2 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -Dmaven.wagon.http.retryHandler.requestSentEnabled=true -Dmaven.wagon.http.retryHandler.count=10
      run: mvn --batch-mode --errors --update-snapshots verify --file pom.xml ${{ steps.maven-profile-flag.outputs.MAVEN_PROFILE_FLAG }}
      run: mvn --batch-mode --errors --update-snapshots verify --file pom.xml ${{ steps.failsafe.outputs.MAVEN_PROFILE_FLAG }}
    - name: Test on Unix
      if: runner.os != 'Windows'
      run:   |
        export OPENDJ_JAVA_ARGS="-server -Xmx1g"
        opendj-server-legacy/target/package/opendj/setup -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --sampleData 50000 --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 --baseDN dc=example,dc=com --sampleData 5000 --cli --acceptLicense --no-prompt
        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 50000
        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/stop-ds
    - name: Test LDAP in Cassandra
      if: runner.os == 'Linux'
      run:   |
        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
        export OPENDJ_JAVA_ARGS="-server -Xmx1g -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/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/stop-ds
    - name: Test on Windows
      if: runner.os == 'Windows'
      run:   |
        set OPENDJ_JAVA_ARGS="-server -Xmx1g"
        opendj-server-legacy\target\package\opendj\setup.bat -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --sampleData 50000 --cli --acceptLicense --no-prompt
        opendj-server-legacy\target\package\opendj\setup.bat -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\bat\status.bat --bindDN "cn=Directory Manager" --bindPassword password
        opendj-server-legacy\target\package\opendj\bat\ldapsearch.bat --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\bat\ldapsearch.bat --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | find /c '"dn:"' | findstr "50000"
        opendj-server-legacy\target\package\opendj\bat\ldapsearch.bat --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | find /c '"dn:"' | findstr "5000"
        opendj-server-legacy\target\package\opendj\bat\stop-ds.bat
    - name: Upload artifacts OpenDJ Server
      uses: actions/upload-artifact@v3
      with:
opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransport.java
@@ -136,7 +136,6 @@
            builder.setReuseAddress(Boolean.parseBoolean(reuseAddressStr));
        }
        builder.setMemoryManager(new PooledMemoryManager(true));
        final TCPNIOTransport transport = builder.build();
opendj-maven-plugin/src/main/resources/config/stylesheets/abbreviations.xsl
@@ -13,6 +13,7 @@
  Copyright 2008-2009 Sun Microsystems, Inc.
  Portions copyright 2011-2016 ForgeRock AS.
  Portions copyright 2023 3A Systems LLC
  ! -->
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
@@ -43,7 +44,7 @@
              or $value = 'des' or $value = 'aes' or $value = 'rc4'
              or $value = 'db' or $value = 'snmp' or $value = 'qos'
              or $value = 'ecl' or $value = 'ttl' or $value = 'jpeg'
              or $value = 'pbkdf2' or $value = 'pkcs5s2' or $value = 'pdb'
              or $value = 'pbkdf2' or $value = 'pkcs5s2' or $value = 'pdb' or $value = 'cas'
             "/>
  </xsl:template>
</xsl:stylesheet>
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CASBackendConfiguration.xml
New file
@@ -0,0 +1,79 @@
<?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 2023- 3A Systems LLC
  ! -->
<adm:managed-object name="cas-backend" plural-name="cas-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 a Apache Cassandra cluster.
  </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-cas-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.cassandra.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 keyspace name
    </adm:synopsis>
    <adm:description>
      The path may be either an absolute path or a path relative to the
      directory containing the base of the <adm:product-name /> directory server
      installation. The path may be any valid directory path in which
      the server has appropriate permissions to read and write files and
      has sufficient space to hold the database contents.
    </adm:description>
    <adm:requires-admin-action>
      <adm:component-restart />
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>ldap_opendj</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>
opendj-server-legacy/pom.xml
@@ -245,6 +245,18 @@
      <artifactId>juel-api</artifactId>
      <version>${juel.version}</version>
    </dependency>
    <dependency>
        <groupId>com.datastax.oss</groupId>
        <artifactId>java-driver-core</artifactId>
        <version>4.17.0</version>
        <exclusions>
            <exclusion>
                <groupId>org.reactivestreams</groupId>
                <artifactId>reactive-streams</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
  </dependencies>
  <build><finalName>${project.groupId}.${project.artifactId}</finalName>
@@ -1216,7 +1228,7 @@
                    <org.opends.test.pauseOnFailure>false</org.opends.test.pauseOnFailure>
                    <org.opends.test.copyClassesToTestPackage>false</org.opends.test.copyClassesToTestPackage>
                  </systemPropertyVariables>
                  <argLine> -Xmx2g @{argLine}</argLine>
                  <argLine> -Xmx1g @{argLine}</argLine>
                  <reuseForks>false</reuseForks>
                  <forkCount>1</forkCount>
                  <parallel>none</parallel>
opendj-server-legacy/resource/schema/02-config.ldif
@@ -15,6 +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
# This file contains the attribute type and objectclass definitions for use
# with the Directory Server configuration.
@@ -6006,6 +6007,12 @@
        ds-cfg-disk-low-threshold $
        ds-cfg-je-property )
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.60142.2.1.2.1
  NAME 'ds-cfg-cas-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
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
@@ -41,6 +41,9 @@
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -616,6 +619,15 @@
    {
      final String tempLogFilePath = tempLogFile.getPath();
      notifyListeners(getFormattedProgress(INFO_GENERAL_PROVIDE_LOG_IN_ERROR.get(tempLogFilePath)));
    //write log
      try {
        notifyListeners(getLineBreak());
        notifyListeners(LocalizableMessage.valueOf(new String(Files.readAllBytes(Paths.get(tempLogFilePath)),"UTF-8")));
      } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      }
      notifyListeners(getLineBreak());
    }
  }
opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/Backend.java
New file
@@ -0,0 +1,16 @@
package org.opends.server.backends.cassandra;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.server.config.server.CASBackendCfg;
import org.opends.server.backends.pluggable.BackendImpl;
import org.opends.server.core.ServerContext;
public class Backend extends BackendImpl<CASBackendCfg>{
      @Override
      protected Storage configureStorage(CASBackendCfg cfg, ServerContext serverContext) throws ConfigException
      {
        return new Storage(cfg, serverContext);
      }
}
opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/Storage.java
New file
@@ -0,0 +1,521 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following 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 3A Systems, LLC.
 */
package org.opends.server.backends.cassandra;
import static org.opends.server.backends.pluggable.spi.StorageUtils.addErrorMessage;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
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.CASBackendCfg;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.Importer;
import org.opends.server.backends.pluggable.spi.ReadOnlyStorageException;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.SequentialCursor;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.StorageStatus;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.UpdateFunction;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
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 com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import com.datastax.oss.driver.api.core.cql.PreparedStatement;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.cql.Statement;
import com.datastax.oss.driver.api.core.servererrors.InvalidQueryException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class Storage implements org.opends.server.backends.pluggable.spi.Storage, ConfigurationChangeListener<CASBackendCfg>{
    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
    //private final ServerContext serverContext;
    private CASBackendCfg config;
    public Storage(CASBackendCfg cfg, ServerContext serverContext) {
        //this.serverContext = serverContext;
        this.config = cfg;
        cfg.addCASChangeListener(this);
    }
    //config
    @Override
    public boolean isConfigurationChangeAcceptable(CASBackendCfg configuration,List<LocalizableMessage> unacceptableReasons) {
        return true;
    }
    @Override
    public ConfigChangeResult applyConfigurationChange(CASBackendCfg cfg) {
        final ConfigChangeResult ccr = new ConfigChangeResult();
        try
        {
            this.config = cfg;
        }
        catch (Exception e)
        {
          addErrorMessage(ccr, LocalizableMessage.raw(stackTraceToSingleLineString(e)));
        }
        return ccr;
    }
    CqlSession session=null;
    final LoadingCache<String,PreparedStatement> prepared=CacheBuilder.newBuilder()
            .expireAfterAccess(Duration.ofMinutes(10))
            .maximumSize(4096)
            .build(new CacheLoader<String,PreparedStatement>(){
                @Override
                public PreparedStatement load(String query) throws Exception {
                    return session.prepare(query);
                }
            });
    ResultSet execute(Statement<?> statement) {
        if (logger.isTraceEnabled()) {
            final ResultSet res=session.execute(statement.setTracing(true));
            logger.trace(LocalizableMessage.raw(
                    "cassandra: %s"
                    ,res.getExecutionInfo().getQueryTrace().getParameters()
                    )
                );
            return res;
        }
        return session.execute(statement);
    }
    AccessMode accessMode=null;
    @Override
    public void open(AccessMode accessMode) throws Exception {
        this.accessMode=accessMode;
        session=CqlSession.builder()
                .withApplicationName("OpenDJ "+config.getDBDirectory()+"."+config.getBackendId())
                .withConfigLoader(DriverConfigLoader.fromDefaults(Storage.class.getClassLoader()))
                .build();
        if (AccessMode.READ_WRITE.equals(accessMode)) {
            execute(prepared.getUnchecked("CREATE KEYSPACE IF NOT EXISTS "+getKeyspaceName()+" WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'};").bind().setExecutionProfileName(profile));
        }
        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"));
        if (session!=null && !session.isClosed()) {
            session.close();
        }
        session=null;
    }
    String getKeyspaceName() {
        return "\""+config.getDBDirectory().replaceAll("[^a-zA-z0-9_]", "_")+"\"";
    }
    String getTableName() {
        return getKeyspaceName()+".\""+config.getBackendId().replaceAll("[^a-zA-z0-9_]", "_")+"\"";
    }
    @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 {
            execute(prepared.getUnchecked("TRUNCATE TABLE "+getTableName()+";").bind().setExecutionProfileName(profile));
        }catch (Throwable e) {}
        if (!isOpen) {
            close();
        }
    }
    //operation
    @Override
    public <T> T read(ReadOperation<T> readOperation) throws Exception {
        return readOperation.run(new TransactionImpl(AccessMode.READ_ONLY));
    }
    @Override
    public void write(WriteOperation writeOperation) throws Exception {
        writeOperation.run(new TransactionImpl(accessMode));
    }
    final static String profile="ddl";
    static {
        if (System.getProperty("datastax-java-driver.basic.request.timeout")==null) {
            System.setProperty("datastax-java-driver.basic.request.timeout", "10 seconds");
        }
        if (System.getProperty("datastax-java-driver.profiles."+profile+".basic.request.timeout")==null) {
            System.setProperty("datastax-java-driver.profiles."+profile+".basic.request.timeout", "30 seconds");
        }
    }
    private final class TransactionImpl implements ReadableTransaction,WriteableTransaction {
        final AccessMode accessMode;
        public TransactionImpl(AccessMode accessMode) {
            super();
            this.accessMode=accessMode;
        }
        @Override
        public void openTree(TreeName name, boolean createOnDemand) {
            if (createOnDemand) {
                execute(prepared.getUnchecked("CREATE TABLE IF NOT EXISTS "+getTableName()+" (baseDN text,indexId text,key blob,value blob,PRIMARY KEY ((baseDN,indexId),key));").bind().setExecutionProfileName(profile));
            }
        }
        public void clearTree(TreeName treeName) {
            checkReadOnly();
            deleteTree(treeName);
        }
        @Override
        public ByteString read(TreeName treeName, ByteSequence key) {
            final Row row=execute(
                    prepared.getUnchecked("SELECT value FROM "+getTableName()+" WHERE baseDN=:baseDN and indexId=:indexId and key=:key").bind()
                        .setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
                        .setByteBuffer("key", ByteBuffer.wrap(key.toByteArray()))
                    ).one();
            return row==null?null:ByteString.wrap(row.getByteBuffer("value").array());
        }
        @Override
        public Cursor<ByteString, ByteString> openCursor(TreeName treeName) {
            return new CursorImpl(this,treeName);
        }
        @Override
        public long getRecordCount(TreeName treeName) {
            return execute(
                    prepared.getUnchecked("SELECT count(*) FROM "+getTableName()+" WHERE baseDN=:baseDN and indexId=:indexId").bind()
                        .setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
                    ).one().getLong(0);
        }
        @Override
        public void deleteTree(TreeName treeName) {
            checkReadOnly();
            openTree(treeName,true);
            execute(
                    prepared.getUnchecked("DELETE FROM "+getTableName()+" WHERE baseDN=:baseDN and indexId=:indexId").bind()
                        .setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
                    );
        }
        @Override
        public void put(TreeName treeName, ByteSequence key, ByteSequence value) {
            checkReadOnly();
            execute(
                prepared.getUnchecked("INSERT INTO "+getTableName()+" (baseDN,indexId,key,value) VALUES (:baseDN,:indexId,:key,:value)").bind()
                    .setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
                    .setByteBuffer("key", ByteBuffer.wrap(key.toByteArray()))
                    .setByteBuffer("value",ByteBuffer.wrap(value.toByteArray()))
                );
        }
        @Override
        public boolean update(TreeName treeName, ByteSequence key, UpdateFunction f) {
            checkReadOnly();
            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) {
            checkReadOnly();
            execute(
                    prepared.getUnchecked("DELETE FROM "+getTableName()+" WHERE baseDN=:baseDN and indexId=:indexId and key=:key").bind()
                        .setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
                        .setByteBuffer("key", ByteBuffer.wrap(key.toByteArray()))
                    );
            return true;
        }
        void checkReadOnly() {
            if (AccessMode.READ_ONLY.equals(accessMode)) {
                throw new ReadOnlyStorageException();
            }
        }
    }
    private final class CursorImpl implements Cursor<ByteString, ByteString> {
        final TreeName treeName;
        final TransactionImpl tx;
        ResultSet rc;
        Iterator<Row> iterator;
        Row current=null;
        public CursorImpl(TransactionImpl tx,TreeName treeName) {
            this.treeName=treeName;
            this.tx=tx;
        }
        ResultSet full(){
            return execute(
                        prepared.getUnchecked("SELECT key,value FROM "+getTableName()+" WHERE baseDN=:baseDN and indexId=:indexId ORDER BY key").bind()
                            .setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
                        );
        }
        @Override
        public boolean next() {
            if (iterator==null) {
                rc=full();
                iterator=rc.iterator();
            }
            try {
                current=iterator.next();
                return true;
            }catch (NoSuchElementException e) {
                current=null;
            }
            return false;
        }
        @Override
        public boolean isDefined() {
            return current!=null;
        }
        @Override
        public ByteString getKey() throws NoSuchElementException {
            if (!isDefined()) {
                throw new NoSuchElementException();
            }
            return ByteString.wrap(current.getByteBuffer("key").array());
        }
        @Override
        public ByteString getValue() throws NoSuchElementException {
            if (!isDefined()) {
                throw new NoSuchElementException();
            }
            return ByteString.wrap(current.getByteBuffer("value").array());
        }
        @Override
        public void delete() throws NoSuchElementException, UnsupportedOperationException {
            if (!isDefined()) {
                throw new NoSuchElementException();
            }
            tx.delete(treeName, getKey());
        }
        @Override
        public void close() {
            iterator=null;
            current=null;
            rc=null;
        }
        ResultSet full(ByteSequence key){
            return execute(
                        prepared.getUnchecked("SELECT key,value FROM "+getTableName()+" WHERE baseDN=:baseDN and indexId=:indexId and key>=:key ORDER BY key").bind()
                            .setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
                            .setByteBuffer("key", ByteBuffer.wrap(key.toByteArray()))
                        );
        }
        @Override
        public boolean positionToKeyOrNext(ByteSequence key) {
            rc=full(key); // start iterator from key key>=:key
            iterator=rc.iterator();
            if (iterator.hasNext()) {
                current=iterator.next();
                return true;
            }
            current=null;
            return false;
        }
        @Override
        public boolean positionToKey(ByteSequence key) {
            if (positionToKeyOrNext(key) && key.equals(getKey())){
                return true;
            }
            current=null;
            return false;
        }
        ResultSet last(){
            return execute(
                        prepared.getUnchecked("SELECT key,value FROM "+getTableName()+" WHERE baseDN=:baseDN and indexId=:indexId ORDER BY key DESC LIMIT 1").bind()
                            .setString("baseDN", treeName.getBaseDN()).setString("indexId", treeName.getIndexId())
                        );
        }
        @Override
        public boolean positionToLastKey() {
            rc=last();
            iterator=rc.iterator();
            if (iterator.hasNext()) {
                current=iterator.next();
                return true;
            }
            current=null;
            return false;
        }
        @Override
        public boolean positionToIndex(int index) {
            iterator=rc.iterator(); //reset position
            int ct=0;
            while(iterator.hasNext()){
                current=iterator.next();
                if (ct==index) {
                    return true;
                }
                ct++;
            }
            current=null;
            return false;
        }
    }
    @Override
    public Set<TreeName> listTrees() {
        // TODO Auto-generated method stub
        return Collections.emptySet();
    }
    private final class ImporterImpl implements Importer {
        final TransactionImpl 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 TransactionImpl(accessMode);
        }
        @Override
        public void close() {
            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 cassandra 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 cassandra export
        //new BackupManager(config.getBackendId()).restoreBackup(this, restoreConfig);
    }
}
opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/package-info.java
New file
@@ -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 2023 3A Systems LLC.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.PRIVATE)
package org.opends.server.backends.cassandra;
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/OnDiskMergeImporter.java
@@ -389,6 +389,7 @@
      final int initialThreadCount = maxThreadCount;
      final Long offheapMemorySize = backendCfg.getImportOffheapMemorySize();
      boolean useOffHeap = (offheapMemorySize != null && offheapMemorySize > 0);
      long memoryAvailable =
          useOffHeap ? offheapMemorySize.longValue() : calculateAvailableHeapMemoryForBuffersAfterGC();
      int threadCount = initialThreadCount;
opendj-server-legacy/src/main/java/org/opends/server/tools/BackendTypeHelper.java
@@ -140,7 +140,11 @@
    try
    {
      Class.forName(backendClassName);
      backends.add(backendToAdd);
      if (backendClassName.equals("org.opends.server.backends.jeb.JEBackend")) { //default
          backends.add(0,backendToAdd);
      }else {
          backends.add(backendToAdd);
      }
    }
    catch (ClassNotFoundException ignored)
    {
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java
@@ -1673,13 +1673,15 @@
    appendStreamContent(logsContents, TestCaseUtils.getSystemOutContents(), "System.out");
    appendStreamContent(logsContents, TestCaseUtils.getSystemErrContents(), "System.err");
    
    for (final File logFile : Arrays.asList(new File(paths.testInstanceRoot, "logs").listFiles())) {
         try {
            appendStreamContent(logsContents, readFile(logFile.getPath()), logFile.getPath());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if (new File(paths.testInstanceRoot, "logs").listFiles()!=null) {
        for (final File logFile : Arrays.asList(new File(paths.testInstanceRoot, "logs").listFiles())) {
             try {
                appendStreamContent(logsContents, readFile(logFile.getPath()), logFile.getPath());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
  }
  private static void appendStreamContent(StringBuilder out, String content, String name)
opendj-server-legacy/src/test/java/org/opends/server/TestListener.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2023 3A Systems, LLC.
 */
package org.opends.server;
@@ -246,7 +247,6 @@
        && countTestsWithStatus(ITestResult.SKIP) != 0) {
      originalSystemErr.println("There were no explicit test failures,"
          + " but some tests were skipped (possibly due to errors in @Before* or @After* methods).");
      System.exit(-1);
    }
  }
opendj-server-legacy/src/test/java/org/opends/server/backends/cassandra/EncryptedTestCase.java
New file
@@ -0,0 +1,62 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2023 3A Systems, LLC.
 */
package org.opends.server.backends.cassandra;
import static org.mockito.Mockito.when;
import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
import org.forgerock.opendj.server.config.server.CASBackendCfg;
import org.opends.server.backends.pluggable.PluggableBackendImplTestCase;
import org.testng.SkipException;
import org.testng.annotations.Test;
import com.datastax.oss.driver.api.core.AllNodesFailedException;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
//docker run --rm -it -p 9042:9042 --name cassandra cassandra
@Test
public class EncryptedTestCase extends PluggableBackendImplTestCase<CASBackendCfg>
{
  @Override
  protected Backend createBackend()
  {
      System.setProperty("datastax-java-driver.basic.request.timeout", "30 seconds"); //for docker slow start
      //test allow cassandra
      try(CqlSession session=CqlSession.builder()
            .withConfigLoader(DriverConfigLoader.fromDefaults(Storage.class.getClassLoader()))
            .build()){
        session.close();
      }catch (AllNodesFailedException e) {
          throw new SkipException("run before test: docker run --rm -it -p 9042:9042 --name cassandra cassandra");
      }
      return new Backend();
  }
  @Override
  protected CASBackendCfg createBackendCfg()
  {
      CASBackendCfg backendCfg = mockCfg(CASBackendCfg.class);
      when(backendCfg.getBackendId()).thenReturn("EncCASTestCase"+System.currentTimeMillis());
      when(backendCfg.getDBDirectory()).thenReturn("EncCASTestCase");
      when(backendCfg.isConfidentialityEnabled()).thenReturn(true);
      when(backendCfg.getCipherKeyLength()).thenReturn(128);
      when(backendCfg.getCipherTransformation()).thenReturn("AES/CBC/PKCS5Padding");
      return backendCfg;
  }
}
opendj-server-legacy/src/test/java/org/opends/server/backends/cassandra/TestCase.java
New file
@@ -0,0 +1,56 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2023 3A Systems, LLC.
 */
package org.opends.server.backends.cassandra;
import static org.mockito.Mockito.when;
import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
import org.forgerock.opendj.server.config.server.CASBackendCfg;
import org.opends.server.backends.pluggable.PluggableBackendImplTestCase;
import org.testng.SkipException;
import org.testng.annotations.Test;
import com.datastax.oss.driver.api.core.AllNodesFailedException;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
//docker run --rm -it -p 9042:9042 --name cassandra cassandra
@Test
public class TestCase extends PluggableBackendImplTestCase<CASBackendCfg> {
    @Override
    protected Backend createBackend() {
        System.setProperty("datastax-java-driver.basic.request.timeout", "30 seconds"); //for docker slow start
        //test allow cassandra
        try(CqlSession session=CqlSession.builder()
                .withConfigLoader(DriverConfigLoader.fromDefaults(Storage.class.getClassLoader()))
                .build()){
            session.close();
        }catch (AllNodesFailedException e) {
            throw new SkipException("run before test: docker run --rm -it -p 9042:9042 --name cassandra cassandra");
        }
        return new Backend();
    }
    @Override
    protected CASBackendCfg createBackendCfg() {
        CASBackendCfg backendCfg = mockCfg(CASBackendCfg.class);
        when(backendCfg.getBackendId()).thenReturn("CASTestCase");
        when(backendCfg.getDBDirectory()).thenReturn("CASTestCase");
        return backendCfg;
    }
}
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java
@@ -12,6 +12,7 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2015-2016 ForgeRock AS.
 * Copyright 2023      3A Systems, LLC.
 */
package org.opends.server.backends.pluggable;
@@ -66,6 +67,7 @@
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.controls.SubtreeDeleteControl;
import org.opends.server.core.AddOperation;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.ModifyDNOperation;
@@ -186,6 +188,12 @@
    backend.configureBackend(backendCfg, TestCaseUtils.getServerContext());
    backend.openBackend();
    if (backend.entryExists(testBaseDN)) {
        DeleteOperation op = mock(DeleteOperation.class);
        when(op.getRequestControl(SubtreeDeleteControl.DECODER)).thenReturn(new SubtreeDeleteControl(true));
        backend.deleteEntry(testBaseDN, op);
    }
    topEntries = TestCaseUtils.makeEntries(
                "dn: " + testBaseDN,
                "objectclass: top",
@@ -550,7 +558,7 @@
    searchDN = entries.get(1).getName();
    badEntryDN = testBaseDN.child(DN.valueOf("ou=bogus")).child(DN.valueOf("ou=dummy"));
    backupID = "backupID1";
    addEntriesToBackend(topEntries);
    addEntriesToBackend(entries);
    addEntriesToBackend(workEntries);
@@ -1171,6 +1179,8 @@
    backend.finalizeBackend();
    try
    {
      readOnlyContainer.open(AccessMode.READ_WRITE); //init storage before reading
      readOnlyContainer.close();
      readOnlyContainer.open(AccessMode.READ_ONLY);
      readOnlyContainer.getStorage().write(new WriteOperation()
      {
pom.xml
@@ -400,7 +400,7 @@
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>3.1.0</version>
                    <version>3.1.2</version>
                </plugin>
                
                <plugin>