blob: 673d2a1476b44aafacd108a2743154f007c861fd [file] [log] [blame]
/*
* Copyright (c) 2012, 2019 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:
*/
@Override
public void addFieldsToRow(AbstractRecord row, AbstractSession session) {
// No fields to add in table per tenant policy.
}
/**
* INTERNAL:
*/
@Override
public void addToTableDefinition(TableDefinition tableDefinition) {
// Nothing to do here. Called during DDL generation.
}
/**
* INTERNAL:
* Multitenant policies are cloned per inheritance subclass.
*/
@Override
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:
*/
@Override
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:
*/
@Override
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:
*/
@Override
public boolean isTablePerMultitenantPolicy() {
return true;
}
/**
* INTERNAL:
*/
@Override
public void postInitialize(AbstractSession session) {
// Nothing to do here.
}
/**
* INTERNAL:
*/
@Override
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<>();
Set<DatabaseTable> secondaryTables = new HashSet<>();
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);
}
}