LegacyScopeIdentityNormalizer.java

package com.taxonomy.shared.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Locale;

/**
 * Canonicalizes legacy tenant identifiers before repository constraints are
 * derived by {@link SchemaContractMigrator}.
 *
 * <p>Repository and entity contexts strip surrounding whitespace. Persisting the
 * same representation during upgrade prevents rows from becoming invisible
 * after restart. Duplicate workspace metadata that collapses to one normalized
 * identity is rejected because its source-repository provenance is ambiguous.</p>
 */
final class LegacyScopeIdentityNormalizer {

    private static final Logger log = LoggerFactory.getLogger(
            LegacyScopeIdentityNormalizer.class);

    private static final String[] TENANT_TABLES = {
            "taxonomy_relation",
            "relation_proposal",
            "relation_hypothesis"
    };

    private final DataSource dataSource;

    LegacyScopeIdentityNormalizer(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    void normalize() {
        try (Connection connection = dataSource.getConnection()) {
            boolean previousAutoCommit = connection.getAutoCommit();
            connection.setAutoCommit(false);
            try {
                rejectAmbiguousWorkspaceMetadata(connection);
                for (String table : TENANT_TABLES) {
                    if (tableExists(connection, table)
                            && columnExists(connection, table, "workspace_id")) {
                        execute(connection, "UPDATE " + table
                                + " SET workspace_id = TRIM(workspace_id)"
                                + " WHERE workspace_id IS NOT NULL");
                        execute(connection, "UPDATE " + table
                                + " SET workspace_id = NULL"
                                + " WHERE workspace_id = ''");
                    }
                }
                if (tableExists(connection, "relation_hypothesis")
                        && columnExists(
                                connection,
                                "relation_hypothesis",
                                "analysis_session_id")) {
                    execute(connection, "UPDATE relation_hypothesis"
                            + " SET analysis_session_id = TRIM(analysis_session_id)"
                            + " WHERE analysis_session_id IS NOT NULL");
                    execute(connection, "UPDATE relation_hypothesis"
                            + " SET analysis_session_id = NULL"
                            + " WHERE analysis_session_id = ''");
                }
                connection.commit();
                log.debug("Normalized legacy workspace and analysis-session identities");
            } catch (SQLException | RuntimeException error) {
                rollbackQuietly(connection, error);
                throw new IllegalStateException(
                        "Unable to normalize legacy tenant identities safely", error);
            } finally {
                connection.setAutoCommit(previousAutoCommit);
            }
        } catch (SQLException error) {
            throw new IllegalStateException(
                    "Unable to open database for tenant identity normalization", error);
        }
    }

    private void rejectAmbiguousWorkspaceMetadata(Connection connection) throws SQLException {
        if (!tableExists(connection, "user_workspace")
                || !columnExists(connection, "user_workspace", "workspace_id")) {
            return;
        }
        String sql = "SELECT COUNT(*) FROM (SELECT TRIM(workspace_id) AS workspace_key"
                + " FROM user_workspace"
                + " WHERE workspace_id IS NOT NULL"
                + " AND TRIM(workspace_id) IS NOT NULL"
                + " GROUP BY TRIM(workspace_id)"
                + " HAVING COUNT(*) > 1) normalized_workspaces";
        try (Statement statement = connection.createStatement();
             ResultSet result = statement.executeQuery(sql)) {
            result.next();
            long duplicates = result.getLong(1);
            if (duplicates > 0) {
                throw new IllegalStateException(
                        "Cannot normalize workspace provenance: found " + duplicates
                                + " ambiguous normalized workspace identity group(s)");
            }
        }
    }

    private static boolean tableExists(Connection connection, String logicalName)
            throws SQLException {
        DatabaseMetaData metadata = connection.getMetaData();
        try (ResultSet tables = metadata.getTables(
                connection.getCatalog(), null, "%", new String[]{"TABLE"})) {
            while (tables.next()) {
                String name = tables.getString("TABLE_NAME");
                if (name != null && name.equalsIgnoreCase(logicalName)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean columnExists(
            Connection connection,
            String logicalTable,
            String logicalColumn) throws SQLException {
        DatabaseMetaData metadata = connection.getMetaData();
        try (ResultSet columns = metadata.getColumns(
                connection.getCatalog(), null, "%", "%")) {
            while (columns.next()) {
                String table = columns.getString("TABLE_NAME");
                String column = columns.getString("COLUMN_NAME");
                if (table != null && column != null
                        && table.toLowerCase(Locale.ROOT)
                                .equals(logicalTable.toLowerCase(Locale.ROOT))
                        && column.toLowerCase(Locale.ROOT)
                                .equals(logicalColumn.toLowerCase(Locale.ROOT))) {
                    return true;
                }
            }
        }
        return false;
    }

    private static void execute(Connection connection, String sql) throws SQLException {
        try (Statement statement = connection.createStatement()) {
            statement.execute(sql);
        }
    }

    private static void rollbackQuietly(Connection connection, Throwable original) {
        try {
            connection.rollback();
        } catch (SQLException rollbackFailure) {
            original.addSuppressed(rollbackFailure);
        }
    }
}