/*******************************************************************************
* Copyright (c) 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:
* 05/01/2015 - 2.6.0 - Lukas Jungmann
* - 455905: initial implementation
******************************************************************************/
package org.eclipse.persistence.testing.tests.jpa.advanced.multitenant;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.sql.DataSource;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.eclipse.persistence.config.EntityManagerProperties;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.descriptors.MultitenantPolicy;
import org.eclipse.persistence.descriptors.SchemaPerMultitenantPolicy;
import org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl;
import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.sessions.DatabaseLogin;
import org.eclipse.persistence.sessions.DatabaseSession;
import org.eclipse.persistence.sessions.server.ServerSession;
import org.eclipse.persistence.testing.framework.junit.JUnitTestCase;
import org.eclipse.persistence.testing.framework.junit.JUnitTestCaseHelper;
import org.eclipse.persistence.testing.models.jpa.advanced.AdvancedTableCreator;
import org.eclipse.persistence.testing.models.jpa.advanced.Customer;
import org.eclipse.persistence.testing.tests.feature.TestDataSource;
/**
* Test for {@link SchemaPerMultitenantPolicy}. Uses two distinct DB schemas
* Currently supported on MySQL only.
*
* @author lukas
*/
public class AdvancedMultiTenantSchemaJunitTest extends JUnitTestCase {
private static boolean skipTest = false;
/** Contains properties required for working with proxy datasource*/
private static Properties emfProperties;
/** Stored in emfProperties but also useful to explicitly switch real
* DB connection during test */
private static ProxyDS proxyDataSource;
/** schema names being used */
private static String schema1, schema2;
/** used to store test specific instance so it can be closed in tearDown */
private EntityManagerFactory emf;
public AdvancedMultiTenantSchemaJunitTest() {
super();
}
public AdvancedMultiTenantSchemaJunitTest(String name) {
super(name);
}
public static Test suite() {
TestSuite suite = new TestSuite();
suite.setName("AdvancedMultiTenantSchemaJunitTest");
suite.addTest(new AdvancedMultiTenantSchemaJunitTest("testSetup"));
suite.addTest(new AdvancedMultiTenantSchemaJunitTest("testPolicyConfigurationDefault"));
suite.addTest(new AdvancedMultiTenantSchemaJunitTest("testPolicyConfigurationCustom"));
suite.addTest(new AdvancedMultiTenantSchemaJunitTest("testSequencing"));
// keep this last, used to drop schema created in testSetup
suite.addTest(new AdvancedMultiTenantSchemaJunitTest("testCleanup"));
return suite;
}
@Override
public String getPersistenceUnitName() {
return "multi-tenant-schema-per-tenant";
}
@Override
public void tearDown() {
if (emf != null) {
if (emf.isOpen()) {
emf.close();
}
}
if (schema1 != null) {
try {
getDatabaseSession().executeNonSelectingSQL("use " + schema1 + ";");
} catch (Throwable t) {
t.printStackTrace();
}
}
}
/**
* test setup
* -uses existing connection to obtain DB info - username, pwd and connection string/schema name
* -creates new DB schema named '$existingSchema+"_MT"'
* -creates 2 data sources (first points to original schema, second to the newly created one)
* and 1 proxy datasource (wraps original DSs and all DB requests are going through this DS)
* -prepares tables in both DSs through the usage of proxy DS
* -stores properties necessary for proper EMF creation in emfProperties field
*/
public void testSetup() {
if (!getPlatform().isMySQL()) {
warning("this test is supported on MySQL only");
return;
}
DatabaseSessionImpl databaseSession = getDatabaseSession();
DatabaseLogin login = getDatabaseSession().getLogin();
schema1 = login.getConnectionString().substring(login.getConnectionString().lastIndexOf('/') + 1);
schema2 = schema1 + "_MT";
assertNotNull(schema1);
assertNotNull(schema2);
databaseSession.executeNonSelectingSQL("use " + schema1 + ";");
try {
databaseSession.executeNonSelectingSQL("drop schema " + schema2 + ";");
} catch (Throwable t) {
// ignore
}
try {
databaseSession.executeNonSelectingSQL("create schema " + schema2 + ";");
} catch (Throwable t) {
// we may not have enough permissions to create additional schema,
// in such case, skip all tests and log a warning
skipTest = true;
warning("Cannot create additional schema to run related tests.");
databaseSession.logThrowable(SessionLog.WARNING, SessionLog.CONNECTION, t);
return;
} finally {
databaseSession.logout();
}
Map<String, String> currentProps = JUnitTestCaseHelper.getDatabaseProperties(getPersistenceUnitName());
TestDataSource ds1 = new TestDataSource(login.getDriverClassName(), login.getConnectionString(), (Properties) login.getProperties().clone());
TestDataSource ds2 = new TestDataSource(login.getDriverClassName(), login.getConnectionString() + "_MT", (Properties) login.getProperties().clone());
proxyDataSource = new ProxyDS(databaseSession, currentProps.get(PersistenceUnitProperties.JDBC_USER), currentProps.get(PersistenceUnitProperties.JDBC_PASSWORD));
proxyDataSource.add(schema1, ds1);
proxyDataSource.add(schema2, ds2);
emfProperties = new Properties();
emfProperties.putAll(currentProps);
// need DS, not real JDBC Connections
emfProperties.remove(PersistenceUnitProperties.JDBC_DRIVER);
emfProperties.remove(PersistenceUnitProperties.JDBC_USER);
emfProperties.remove(PersistenceUnitProperties.JDBC_URL);
emfProperties.remove(PersistenceUnitProperties.JDBC_PASSWORD);
emfProperties.put(PersistenceUnitProperties.NON_JTA_DATASOURCE, proxyDataSource);
emfProperties.put(PersistenceUnitProperties.DDL_GENERATION, PersistenceUnitProperties.NONE);
emfProperties.put(PersistenceUnitProperties.MULTITENANT_STRATEGY, "external");
// prepare 1st tenant
proxyDataSource.setCurrentDS(schema1);
EntityManagerFactory emf = Persistence.createEntityManagerFactory(getPersistenceUnitName(), emfProperties);
assertNotNull(emf);
new AdvancedTableCreator().replaceTables(((EntityManagerFactoryImpl) emf).getServerSession());
emf.close();
// prepare 2nd tenant
proxyDataSource.setCurrentDS(schema2);
emf = Persistence.createEntityManagerFactory(getPersistenceUnitName(), emfProperties);
assertNotNull(emf);
new AdvancedTableCreator().replaceTables(((EntityManagerFactoryImpl) emf).getServerSession());
emf.close();
}
public void testPolicyConfigurationDefault() {
if (!getPlatform().isMySQL()) {
warning("this test is supported on MySQL only");
return;
}
if (skipTest) {
warning("this test requires DB schema created in testSetup to be available.");
return;
}
// default configuration: shared EMF = true, shared cache = false
// strategy = 'external'
emf = Persistence.createEntityManagerFactory(getPersistenceUnitName(), emfProperties);
ServerSession session = ((EntityManagerFactoryImpl) emf).getServerSession();
MultitenantPolicy policy = session.getProject().getMultitenantPolicy();
assertNotNull("project MultitenantPolicy is null", policy);
assertTrue("not SchemaPerMultitenantPolicy", policy.isSchemaPerMultitenantPolicy());
assertTrue("not shared EMF", ((SchemaPerMultitenantPolicy) policy).shouldUseSharedEMF());
assertFalse("shared cache", ((SchemaPerMultitenantPolicy) policy).shouldUseSharedCache());
assertEquals(PersistenceUnitProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT, ((SchemaPerMultitenantPolicy) policy).getContextProperty());
assertEquals(EntityManagerProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT, ((SchemaPerMultitenantPolicy) policy).getContextProperty());
assertTrue("unknown context property", session.getMultitenantContextProperties().contains(PersistenceUnitProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT));
}
public void testPolicyConfigurationCustom() {
if (!getPlatform().isMySQL()) {
warning("this test is supported on MySQL only");
return;
}
if (skipTest) {
warning("this test requires DB schema created in testSetup to be available.");
return;
}
// custom configuration: shared EMF = false, shared cache = true
Properties emfP = new Properties();
emfP.putAll(emfProperties);
emfP.put(PersistenceUnitProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT, "not-shared");
emfP.put(PersistenceUnitProperties.MULTITENANT_SHARED_EMF, "false");
emfP.put(PersistenceUnitProperties.MULTITENANT_SHARED_CACHE, "true");
emf = Persistence.createEntityManagerFactory(getPersistenceUnitName(), emfP);
ServerSession session = ((EntityManagerFactoryImpl) emf).getServerSession();
MultitenantPolicy policy = session.getProject().getMultitenantPolicy();
assertNotNull("project MultitenantPolicy is null", policy);
assertTrue("not SchemaPerMultitenantPolicy", policy.isSchemaPerMultitenantPolicy());
assertFalse("shared EMF", ((SchemaPerMultitenantPolicy) policy).shouldUseSharedEMF());
assertTrue("not shared cache", ((SchemaPerMultitenantPolicy) policy).shouldUseSharedCache());
assertEquals(PersistenceUnitProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT, ((SchemaPerMultitenantPolicy) policy).getContextProperty());
assertEquals(EntityManagerProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT, ((SchemaPerMultitenantPolicy) policy).getContextProperty());
assertTrue("unknown context property", session.getMultitenantContextProperties().contains(PersistenceUnitProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT));
}
/**
* -uses shared EMF, proxy DS
* -creates EM, 2 entities and persists them reaching default schema
* -switches DB context to another schema
* -creates EM, 2 entities and persists them reaching second schema
*
* sequencing in 2nd schema should start from the beginning,
* not from some state in the 1st schema; both should grow by 1
*/
public void testSequencing() {
if (!getPlatform().isMySQL()) {
warning("this test is supported on MySQL only");
return;
}
if (skipTest) {
warning("this test requires DB schema created in testSetup to be available.");
return;
}
emf = Persistence.createEntityManagerFactory(getPersistenceUnitName(), emfProperties);
Customer customer1 = new Customer();
customer1.setLastName("Simpson");
customer1.setFirstName("Bart");
Customer customer1a = new Customer();
customer1a.setLastName("Simpson");
customer1a.setFirstName("Homer");
proxyDataSource.setCurrentDS(schema1);
Properties props1 = new Properties();
props1.put(EntityManagerProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT, "Simpsons");
EntityManager em1 = emf.createEntityManager(props1);
em1.getTransaction().begin();
try {
em1.persist(customer1);
em1.persist(customer1a);
em1.getTransaction().commit();
} catch (Throwable t) {
if (em1.getTransaction().isActive()) {
em1.getTransaction().rollback();
}
} finally {
em1.close();
}
int id1 = customer1.getId();
int id1a = customer1a.getId();
Customer customer2 = new Customer();
customer2.setLastName("Cooper");
customer2.setFirstName("Sheldon");
Customer customer2a = new Customer();
customer2a.setLastName("Hofstadter");
customer2a.setFirstName("Leonard");
proxyDataSource.setCurrentDS(schema2);
Properties props2 = new Properties();
props2.put(EntityManagerProperties.MULTITENANT_SCHEMA_PROPERTY_DEFAULT, "TBBT");
EntityManager em2 = emf.createEntityManager(props2);
em2.getTransaction().begin();
try {
em2.persist(customer2);
em2.persist(customer2a);
em2.getTransaction().commit();
} catch (Throwable t) {
if (em2.getTransaction().isActive()) {
em2.getTransaction().rollback();
}
} finally {
em2.close();
}
int id2 = customer2.getId();
int id2a = customer2a.getId();
// cannot rely on any value in schema1 as there may or may not be some tests run earlier
// which would move the sequence or maybe failed before preallocating/moving the sequence
// but what we know for sure is that IDs in the second schema should start from
// the beginning (with 50) regardless of values in the first schema
assertEquals(50, id2);
assertEquals(51, id2a);
// and IDs in both schemas should grow by 1
assertEquals("IDs should grow by 1", id1 + 1, id1a);
assertEquals("IDs should grow by 1", id2 + 1, id2a);
}
public void testCleanup() {
try {
getDatabaseSession(getPersistenceUnitName()).executeNonSelectingSQL("drop schema " + schema2 + ";");
} catch (Throwable t) {
// ignore
}
}
static final class ProxyDS implements DataSource {
private final Map<String, TestDataSource> datasources;
private DataSource currentDS;
private DatabaseSession session;
private final String userName, pwd;
public ProxyDS(DatabaseSession session, String userName, String pwd) {
datasources = new HashMap<>();
this.session = session;
this.userName = userName;
this.pwd = pwd;
}
public void add(String key, TestDataSource ds) {
datasources.put(key, ds);
currentDS = ds;
}
public void setCurrentDS(String key) {
session.executeNonSelectingSQL("use " + key + "");
currentDS = datasources.get(key);
}
public DataSource getCurrentDS() {
return currentDS;
}
@Override
public Connection getConnection() throws SQLException {
return getConnection(userName, pwd);
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return currentDS.getConnection(username, password);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return currentDS.getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
currentDS.setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
currentDS.setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return currentDS.getLoginTimeout();
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return currentDS.getParentLogger();
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return currentDS.unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return currentDS.isWrapperFor(iface);
}
}
}