/*
 * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     14/05/2012-2.4 Guy Pelletier
//       - 376603: Provide for table per tenant support for multitenant applications
package org.eclipse.persistence.descriptors;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import org.eclipse.persistence.annotations.TenantTableDiscriminatorType;
import org.eclipse.persistence.config.EntityManagerProperties;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.helper.NonSynchronizedVector;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.DirectCollectionMapping;
import org.eclipse.persistence.mappings.ManyToManyMapping;
import org.eclipse.persistence.mappings.OneToOneMapping;
import org.eclipse.persistence.tools.schemaframework.TableDefinition;

/**
 * A table per tenant multitenant policy. Tables can either be per schema
 * or augmented with a prefix or suffix per tenant.
 *
 * @author Guy Pelletier
 * @since EclipseLink 2.4
 */
public class TablePerMultitenantPolicy implements MultitenantPolicy, Cloneable {
    protected ClassDescriptor descriptor;

    // Maps original tables with tenant per table ones. This map is used when
    // building fields that referred to the original table.
    protected Map<DatabaseTable, DatabaseTable> tablePerTenantTables;
    protected TenantTableDiscriminatorType type;
    protected String contextProperty;
    protected String contextTenant;

    TablePerMultitenantPolicy() {
    }

    public TablePerMultitenantPolicy(ClassDescriptor desc) {
        descriptor = desc;
        type = TenantTableDiscriminatorType.SUFFIX;
        tablePerTenantTables = new HashMap(4);
        contextProperty = EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT;
    }

    /**
     * INTERNAL:
     */
    public void addFieldsToRow(AbstractRecord row, AbstractSession session) {
        // No fields to add in table per tenant policy.
    }

    /**
     * INTERNAL:
     */
    public void addToTableDefinition(TableDefinition tableDefinition) {
        // Nothing to do here. Called during DDL generation.
    }

    /**
     * INTERNAL:
     * Multitenant policies are cloned per inheritance subclass.
     */
    public MultitenantPolicy clone(ClassDescriptor descriptor) {
        TablePerMultitenantPolicy clonedPolicy = null;

        try {
            clonedPolicy = (TablePerMultitenantPolicy) super.clone();

            clonedPolicy.descriptor = descriptor;

            // Create a separate hashmap per clone.
            clonedPolicy.tablePerTenantTables = new HashMap(4);
            for (DatabaseTable table : this.tablePerTenantTables.keySet()) {
                clonedPolicy.tablePerTenantTables.put(table, this.tablePerTenantTables.get(table));
            }
        } catch (CloneNotSupportedException exception) {
            throw new InternalError(exception.getMessage());
        }

        return clonedPolicy;
    }

    /**
     * INTERNAL:
     * Return the context property for this table per tenant policy.
     */
    public String getContextProperty() {
        return contextProperty;
    }

    /**
     * INTERNAL:
     * Return the new database table associated with this tenant.
     */
    public DatabaseTable getTable(String tableName) {
        return tablePerTenantTables.get(new DatabaseTable(tableName));
    }

    /**
     * INTERNAL:
     * Return the new database table associated with this tenant.
     */
    public DatabaseTable getTable(DatabaseTable table) {
        return tablePerTenantTables.get(table);
    }

    /**
     * INTERNAL:
     * Return the tenant table name.
     */
    protected String getTableName(DatabaseTable table, String tenant) {
        if (isPrefixPerTable()) {
            return tenant + "_" + table.getName();
        } else {
            return table.getName() + "_" + tenant;
        }
    }

    /**
     * INTERNAL:
     * Return true if the tenant has been set for this policy.
     */
    public boolean hasContextTenant() {
        return this.contextTenant != null;
    }

    /**
     * INTERNAL:
     */
    public void initialize(AbstractSession session) throws DescriptorException {
        // Add the context property to the session set.
        session.addMultitenantContextProperty(contextProperty);
    }

    /**
     * PUBLIC:
     * Return true if this descriptor requires a prefix to the table per tenant.
     */
    public boolean isPrefixPerTable() {
        return type == TenantTableDiscriminatorType.PREFIX;
    }

    /**
     * PUBLIC:
     * Return true if this descriptor requires a table schema per tenant.
     */
    public boolean isSchemaPerTable() {
        return type == TenantTableDiscriminatorType.SCHEMA;
    }

    /**
     * INTERNAL:
     */
    public boolean isSingleTableMultitenantPolicy() {
        return false;
    }

    /**
     * INTERNAL:
     */
    @Override
    public boolean isSchemaPerMultitenantPolicy() {
        return false;
    }

    /**
     * PUBLIC:
     * Return true if this descriptor requires a suffix to the table per tenant.
     */
    public boolean isSuffixPerTable() {
        return (type == null || type == TenantTableDiscriminatorType.SUFFIX);
    }

    /**
     * INTERNAL:
     */
    public boolean isTablePerMultitenantPolicy() {
        return true;
    }

    /**
     * INTERNAL:
     */
    public void postInitialize(AbstractSession session) {
        // Nothing to do here.
    }

    /**
     * INTERNAL:
     */
    public void preInitialize(AbstractSession session) throws DescriptorException {
        // Nothing to do here.
    }

    /**
     * PUBLIC:
     * Set the tenant table discriminator type.
     */
    public void setTenantTableDiscriminatorType(TenantTableDiscriminatorType type) {
        this.type = type;
    }

    /**
     * PUBLIC:
     * Set the context property used to define the table per tenant. If it is
     * not set by the user, the policy defaults it to the multitenant property
     * default of "eclipselink.tenant-id"
     */
    public void setContextProperty(String contextProperty) {
        this.contextProperty = contextProperty;
    }

    /**
     * INTERNAL:
     * This method is used to update the table per tenant descriptor with
     * a table per tenant prefix or suffix on its associated tables. This
     * includes any relation tables from mappings.
     *
     * If the given session is a client session than we must clone the tables.
     * Outside of a client session, assume global usage and no cloning is
     * needed.
     *
     * This method should only be called at the start of a client session
     * lifecycle and should only be called once.
     */
    protected void setTablePerTenant() {
        // Update the descriptor tables.
        Vector<DatabaseTable> tables = NonSynchronizedVector.newInstance(3);
        for (DatabaseTable table : descriptor.getTables()) {
            tables.add(updateTable(table));
        }
        descriptor.setTables(tables);

        // Multitple table foreign keys need to be updated as well
        Map<DatabaseTable, Set<DatabaseTable>> existingMultipleTables = descriptor.getMultipleTableForeignKeys();

        if (existingMultipleTables != null && ! existingMultipleTables.isEmpty()) {
            Map<DatabaseTable, Set<DatabaseTable>> updatedMultipleTables = new HashMap<DatabaseTable, Set<DatabaseTable>>();
            Set<DatabaseTable> secondaryTables = new HashSet<DatabaseTable>();

            for (DatabaseTable table : existingMultipleTables.keySet()) {
                for (DatabaseTable secondaryTable : existingMultipleTables.get(table)) {
                    DatabaseTable updatedSecondaryTable = getTable(secondaryTable);

                    if (updatedSecondaryTable == null) {
                        secondaryTables.add(secondaryTable);
                    } else {
                        secondaryTables.add(updatedSecondaryTable);
                    }
                }

                DatabaseTable updatedTable = getTable(table);

                if (updatedTable == null) {
                    updatedMultipleTables.put(table, secondaryTables);
                } else {
                    updatedMultipleTables.put(updatedTable, secondaryTables);
                }
            }

            descriptor.setMultipleTableForeignKeys(updatedMultipleTables);
        }

        // Any mapping (owning) with a relation table will need to be updated.
        // Non-owning sides of a bidirectional mapping will be updated during
        // descriptor initialization.
        for (DatabaseMapping mapping : descriptor.getMappings()) {
            if (mapping.isManyToManyMapping()) {
                // If the mapping is read only we are not the owner of the
                // relationship meaning we need to look up the relation table
                // name from the reference descriptor. This will be done later
                // on in the initialization phase of the table mechanism (when
                // a reference descriptor has been set and we can look up the
                // correct relation table)
                if (! mapping.isReadOnly()) {
                    ((ManyToManyMapping) mapping).setRelationTable(updateTable(((ManyToManyMapping) mapping).getRelationTable()));
                }
            } else if (mapping.isOneToOneMapping() && ((OneToOneMapping) mapping).hasRelationTable()) {
                ((OneToOneMapping) mapping).setRelationTable(updateTable(((OneToOneMapping) mapping).getRelationTable()));
            } else if (mapping.isDirectCollectionMapping()) {
                ((DirectCollectionMapping) mapping).setReferenceTable(updateTable(((DirectCollectionMapping) mapping).getReferenceTable()));
            }
        }
    }

    /**
     * INTERNAL:
     * This method is used to update the table per tenant descriptor with
     * a table per tenant schema. This includes any relation tables from
     * mappings. This will be done through the setting of a table qualifier on
     * the tables.
     *
     * This method should only be called at the start of a client session
     * lifecycle and should only be called once.
     */
    protected void setTableSchemaPerTenant() {
        descriptor.setTableQualifier(contextTenant);

        // Any mapping with a relation table will need to be updated.
        for (DatabaseMapping mapping : descriptor.getMappings()) {
            if (mapping.isManyToManyMapping()) {
                ((ManyToManyMapping) mapping).getRelationTable().setTableQualifier(contextTenant);
            } else if (mapping.isOneToOneMapping() && ((OneToOneMapping) mapping).hasRelationTable()) {
                ((OneToOneMapping) mapping).getRelationTable().setTableQualifier(contextTenant);
            } else if (mapping.isDirectCollectionMapping()) {
                ((DirectCollectionMapping) mapping).getReferenceTable().setTableQualifier(contextTenant);
            }
        }
    }

    /**
     * INTERNAL:
     */
    public void setContextTenant(String contextTenant) {
        this.contextTenant = contextTenant;

        if (isSchemaPerTable()) {
            setTableSchemaPerTenant();
        } else {
            setTablePerTenant();
        }
    }

    /**
     * INTERNAL:
     * This method is called during regular descriptor initialization. When
     * initializing at that level no cloning should be done on when setting
     * the context tenant.
     */
    public boolean shouldInitialize(AbstractSession session) {
        if (! hasContextTenant()) {
            // If we find our tenant property off the given session, update
            // our descriptors tables and return true for initialization.
            String tenant = (String) session.getProperty(contextProperty);

            if (tenant != null) {
                setContextTenant(tenant);
            }
        }

        return hasContextTenant();
    }

    /**
     * INTERNAL:
     * This method will update the table by cloning it and setting a new name
     * on it. The table association will be stored here as well for ease of
     * future look up.
     */
    protected DatabaseTable updateTable(DatabaseTable table) {
        DatabaseTable tableClone = table.clone();
        tablePerTenantTables.put(table, tableClone);
        tableClone.setName(getTableName(tableClone, contextTenant));
        return tableClone;
    }

    /**
     * INTERNAL:
     * Return true if this policy accepts the given property.
     */
    public boolean usesContextProperty(String property) {
        return contextProperty.equals(property);
    }
}
