/******************************************************************************* * Copyright (c) 2012, 2015 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 v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * 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); } }