PrometheusDataStore.java

/*
 * Prometheus: Application Framework
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package io.github.tonywasher.joceanus.prometheus.database;

import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogManager;
import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogger;
import io.github.tonywasher.joceanus.oceanus.profile.OceanusProfile;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataSet;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataSet.PrometheusCryptographyDataType;
import io.github.tonywasher.joceanus.prometheus.exc.PrometheusIOException;
import io.github.tonywasher.joceanus.tethys.api.thread.TethysUIThreadStatusReport;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Properties;

/**
 * Class that encapsulates a database connection.
 */
public abstract class PrometheusDataStore {
    /**
     * Number of update steps per table (INSERT/UPDATE/DELETE).
     */
    private static final int NUM_STEPS_PER_TABLE = 3;

    /**
     * User property name.
     */
    private static final String PROPERTY_USER = "user";

    /**
     * Password property name.
     */
    private static final String PROPERTY_PASS = "password";

    /**
     * Instance property name.
     */
    private static final String PROPERTY_INSTANCE = "instance";

    /**
     * Encrypt property name.
     */
    private static final String PROPERTY_ENCRYPT = "encrypt";

    /**
     * Logger.
     */
    private static final OceanusLogger LOGGER = OceanusLogManager.getLogger(PrometheusDataStore.class);

    /**
     * Database connection.
     */
    private Connection theConn;

    /**
     * Database name.
     */
    private String theDatabase;

    /**
     * Batch Size.
     */
    private final Integer theBatchSize;

    /**
     * Database Driver.
     */
    private final PrometheusJDBCDriver theDriver;

    /**
     * List of Database tables.
     */
    private final List<PrometheusTableDataItem<?>> theTables;

    /**
     * Construct a new Database class.
     *
     * @param pDatabase the database
     * @param pConfig   the config
     * @throws OceanusException on error
     */
    protected PrometheusDataStore(final String pDatabase,
                                  final PrometheusDBConfig pConfig) throws OceanusException {
        /* Create the connection */
        try {
            /* Access the batch size */
            theBatchSize = pConfig.getBatchSize();

            /* Access the JDBC Driver */
            theDriver = pConfig.getDriver();

            /* Store the name */
            theDatabase = pDatabase;

            /* Obtain the connection */
            final String myConnString = theDriver.getConnectionString(pDatabase, pConfig.getServer(), pConfig.getPort());

            /* Create the properties and record user */
            final Properties myProperties = new Properties();
            final String myUser = pConfig.getUser();
            final char[] myPass = pConfig.getPassword();
            myProperties.setProperty(PROPERTY_USER, myUser);
            myProperties.setProperty(PROPERTY_PASS, new String(myPass));

            /* If we are using instance */
            if (theDriver.useInstance()) {
                final String myInstance = pConfig.getInstance();
                myProperties.setProperty(PROPERTY_INSTANCE, myInstance);
                myProperties.setProperty(PROPERTY_ENCRYPT, "false");
            }

            /* Connect using properties */
            theConn = DriverManager.getConnection(myConnString, myProperties);

            /* Connect to the correct database */
            theConn.setCatalog(pDatabase);

            /* Switch off autoCommit */
            theConn.setAutoCommit(false);

            /* handle exceptions */
        } catch (SQLException e) {
            throw new PrometheusIOException("Failed to load driver", e);
        }

        /* Create table list and add the tables to the list */
        theTables = new ArrayList<>();

        /* Loop through the tables */
        for (PrometheusCryptographyDataType myType : PrometheusCryptographyDataType.values()) {
            /* Create the sheet */
            theTables.add(newTable(myType));
        }
    }

    /**
     * Construct a new Database class.
     *
     * @param pConfig the config
     * @throws OceanusException on error
     */
    protected PrometheusDataStore(final PrometheusDBConfig pConfig) throws OceanusException {
        /* Create the connection */
        try {
            /* Access the batch size */
            theBatchSize = pConfig.getBatchSize();

            /* Access the JDBC Driver */
            theDriver = pConfig.getDriver();

            /* Obtain the connection */
            final String myConnString = theDriver.getConnectionString(pConfig.getServer(), pConfig.getPort());

            /* Create the properties and record user */
            final Properties myProperties = new Properties();
            final String myUser = pConfig.getUser();
            final char[] myPass = pConfig.getPassword();
            myProperties.setProperty(PROPERTY_USER, myUser);
            myProperties.setProperty(PROPERTY_PASS, new String(myPass));

            /* If we are using instance */
            if (theDriver.useInstance()) {
                final String myInstance = pConfig.getInstance();
                myProperties.setProperty(PROPERTY_INSTANCE, myInstance);
                myProperties.setProperty(PROPERTY_ENCRYPT, "false");
            }

            /* Connect using properties */
            theConn = DriverManager.getConnection(myConnString, myProperties);

            /* Switch off autoCommit */
            theConn.setAutoCommit(false);

            /* handle exceptions */
        } catch (SQLException e) {
            throw new PrometheusIOException("Failed to load driver", e);
        }

        /* Create table list and add the tables to the list */
        theTables = new ArrayList<>();

        /* Loop through the tables */
        for (PrometheusCryptographyDataType myType : PrometheusCryptographyDataType.values()) {
            /* Create the sheet */
            theTables.add(newTable(myType));
        }
    }

    /**
     * Obtain the database name.
     *
     * @return the name
     */
    public String getName() {
        return theDatabase;
    }

    /**
     * Execute the statement outside a transaction.
     *
     * @param pStatement the statement
     * @throws OceanusException on error
     */
    void executeStatement(final String pStatement) throws OceanusException {
        /* Protect the statement and execute without commit */
        try (PreparedStatement myStmt = theConn.prepareStatement(pStatement)) {
            theConn.setAutoCommit(true);
            myStmt.execute();
            theConn.setAutoCommit(false);

        } catch (SQLException e) {
            throw new PrometheusIOException("Failed to execute statement", e);
        }
    }

    /**
     * Create new sheet of required type.
     *
     * @param pListType the list type
     * @return the new sheet
     */
    private PrometheusTableDataItem<?> newTable(final PrometheusCryptographyDataType pListType) {
        /* Switch on list Type */
        switch (pListType) {
            case CONTROLDATA:
                return new PrometheusTableControlData(this);
            case CONTROLKEY:
                return new PrometheusTableControlKeys(this);
            case CONTROLKEYSET:
                return new PrometheusTableControlKeySet(this);
            case DATAKEYSET:
                return new PrometheusTableDataKeySet(this);
            default:
                throw new IllegalArgumentException(pListType.toString());
        }
    }

    /**
     * Obtain the Driver.
     *
     * @return the driver
     */
    protected PrometheusJDBCDriver getDriver() {
        return theDriver;
    }

    /**
     * Access the connection.
     *
     * @return the connection
     */
    protected Connection getConn() {
        return theConn;
    }

    /**
     * Add a table.
     *
     * @param pTable the Table to add
     */
    protected void addTable(final PrometheusTableDataItem<?> pTable) {
        pTable.getDefinition().resolveReferences(theTables);
        theTables.add(pTable);
    }

    /**
     * Close the connection to the database rolling back any outstanding transaction.
     */
    public void close() {
        /* Ignore if no connection */
        if (theConn == null) {
            return;
        }

        /* Protect against exceptions */
        try {
            /* Roll-back any outstanding transaction */
            if (!theConn.getAutoCommit()) {
                theConn.rollback();
            }

            /* Loop through the tables */
            for (PrometheusTableDataItem<?> myTable : theTables) {
                /* Close the Statement */
                myTable.closeStmt();
            }

            /* Close the connection */
            theConn.close();
            theConn = null;

            /* Discard Exceptions */
        } catch (SQLException e) {
            LOGGER.error("Failed to close database connection", e);
            theConn = null;
        }
    }

    /**
     * Load data from the database.
     *
     * @param pReport the report
     * @param pData   the new DataSet
     * @throws OceanusException on error
     */
    public void loadDatabase(final TethysUIThreadStatusReport pReport,
                             final PrometheusDataSet pData) throws OceanusException {
        /* Initialise task */
        pReport.initTask("loadDatabase");
        pReport.setNumStages(theTables.size());

        /* Obtain the active profile */
        OceanusProfile myTask = pReport.getActiveTask();
        myTask = myTask.startTask("loadDatabase");

        /* Loop through the tables */
        for (PrometheusTableDataItem<?> myTable : theTables) {
            /* Note the new step */
            myTask.startTask(myTable.getTableName());

            /* Load the items */
            myTable.loadItems(pReport, pData);
        }

        /* Complete the task */
        myTask.end();
    }

    /**
     * Update data into database.
     *
     * @param pReport the report
     * @param pData   the data
     * @throws OceanusException on error
     */
    public void updateDatabase(final TethysUIThreadStatusReport pReport,
                               final PrometheusDataSet pData) throws OceanusException {
        /* Set the number of stages */
        final PrometheusBatchControl myBatch = new PrometheusBatchControl(theBatchSize);
        pReport.setNumStages(NUM_STEPS_PER_TABLE * theTables.size());

        /* Obtain the active profile */
        OceanusProfile myTask = pReport.getActiveTask();
        myTask = myTask.startTask("updateDatabase");

        /* Loop through the tables */
        OceanusProfile myStage = myTask.startTask("insertData");
        final Iterator<PrometheusTableDataItem<?>> myIterator = theTables.iterator();
        while (myIterator.hasNext()) {
            final PrometheusTableDataItem<?> myTable = myIterator.next();

            /* Note the new step */
            myStage.startTask(myTable.getTableName());

            /* insert the items */
            myTable.insertItems(pReport, pData, myBatch);
        }

        /* Loop through the tables */
        myStage = myTask.startTask("updateData");
        final ListIterator<PrometheusTableDataItem<?>> myListIterator = theTables.listIterator();
        while (myListIterator.hasNext()) {
            final PrometheusTableDataItem<?> myTable = myListIterator.next();

            /* Note the new step */
            myStage.startTask(myTable.getTableName());

            /* Load the items */
            myTable.updateItems(pReport, myBatch);
        }

        /* Loop through the tables in reverse order */
        myStage = myTask.startTask("deleteData");
        while (myListIterator.hasPrevious()) {
            final PrometheusTableDataItem<?> myTable = myListIterator.previous();

            /* Note the new step */
            myStage.startTask(myTable.getTableName());

            /* Delete items from the table */
            myTable.deleteItems(pReport, myBatch);
        }

        /* If we have active work in the batch */
        if (myBatch.isActive()) {
            /* Commit the database */
            try {
                theConn.commit();
            } catch (SQLException e) {
                close();
                throw new PrometheusIOException("Failed to commit transaction", e);
            }

            /* Commit the batch */
            myBatch.commitItems();
        }

        /* Complete the task */
        myTask.end();
    }

    /**
     * Create database.
     *
     * @param pReport   the report
     * @param pDatabase the database to create
     * @throws OceanusException on error
     */
    public void createDatabase(final TethysUIThreadStatusReport pReport,
                               final String pDatabase) throws OceanusException {
        /* Set the number of stages */
        pReport.setNumStages(2);

        /* Obtain the active profile */
        OceanusProfile myTask = pReport.getActiveTask();
        myTask = myTask.startTask("dropDatabase");
        executeStatement("DROP DATABASE IF EXISTS " + pDatabase);

        /* Create database */
        myTask = myTask.startTask("createDatabase");
        executeStatement("CREATE DATABASE " + pDatabase);

        /* Complete the task */
        myTask.end();
    }

    /**
     * Create tables.
     *
     * @param pReport the report
     * @throws OceanusException on error
     */
    public void createTables(final TethysUIThreadStatusReport pReport) throws OceanusException {
        /* Set the number of stages */
        pReport.setNumStages(2);

        /* Drop any existing tables */
        dropTables(pReport);

        /* Obtain the active profile */
        OceanusProfile myTask = pReport.getActiveTask();
        myTask = myTask.startTask("createTables");

        /* Loop through the tables */
        final Iterator<PrometheusTableDataItem<?>> myIterator = theTables.iterator();
        while (myIterator.hasNext()) {
            final PrometheusTableDataItem<?> myTable = myIterator.next();

            /* Check for cancellation */
            pReport.checkForCancellation();

            /* Note the new step */
            myTask.startTask(myTable.getTableName());

            /* Create the table */
            myTable.createTable();
        }

        /* Complete the task */
        myTask.end();
    }

    /**
     * Drop tables.
     *
     * @param pReport the report
     * @throws OceanusException on error
     */
    private void dropTables(final TethysUIThreadStatusReport pReport) throws OceanusException {
        /* Obtain the active profile */
        OceanusProfile myTask = pReport.getActiveTask();
        myTask = myTask.startTask("dropTables");

        /* Loop through the tables in reverse order */
        final ListIterator<PrometheusTableDataItem<?>> myIterator = theTables.listIterator(theTables.size());
        while (myIterator.hasPrevious()) {
            final PrometheusTableDataItem<?> myTable = myIterator.previous();

            /* Check for cancellation */
            pReport.checkForCancellation();

            /* Note the new step */
            myTask.startTask(myTable.getTableName());

            /* Drop the table */
            myTable.dropTable();
        }

        /* Complete the task */
        myTask.end();
    }

    /**
     * Purge tables.
     *
     * @param pReport the report
     * @throws OceanusException on error
     */
    public void purgeTables(final TethysUIThreadStatusReport pReport) throws OceanusException {
        /* Set the number of stages */
        pReport.setNumStages(1);

        /* Obtain the active profile */
        OceanusProfile myTask = pReport.getActiveTask();
        myTask = myTask.startTask("purgeTables");

        /* Loop through the tables in reverse order */
        final ListIterator<PrometheusTableDataItem<?>> myIterator = theTables.listIterator(theTables.size());
        while (myIterator.hasPrevious()) {
            final PrometheusTableDataItem<?> myTable = myIterator.previous();

            /* Check for cancellation */
            pReport.checkForCancellation();

            /* Note the new step */
            myTask.startTask(myTable.getTableName());

            /* Purge the table */
            myTable.purgeTable();
        }

        /* Complete the task */
        myTask.end();
    }
}