| /* |
| * 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); |
| } |
| } |