/*
* Copyright 2014 - 2017 Blazebit.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.blazebit.persistence.testsuite.base;
import com.blazebit.persistence.Criteria;
import com.blazebit.persistence.CriteriaBuilderFactory;
import com.blazebit.persistence.spi.CriteriaBuilderConfiguration;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceException;
import javax.persistence.spi.PersistenceProvider;
import javax.persistence.spi.PersistenceProviderResolver;
import javax.persistence.spi.PersistenceProviderResolverHolder;
import javax.persistence.spi.PersistenceUnitInfo;
import javax.persistence.spi.PersistenceUnitTransactionType;
import com.blazebit.persistence.spi.JpaProvider;
import com.blazebit.persistence.spi.JpaProviderFactory;
import com.blazebit.persistence.testsuite.base.cleaner.DB2DatabaseCleaner;
import com.blazebit.persistence.testsuite.base.cleaner.DatabaseCleaner;
import com.blazebit.persistence.testsuite.base.cleaner.H2DatabaseCleaner;
import com.blazebit.persistence.testsuite.base.cleaner.MySQLDatabaseCleaner;
import com.blazebit.persistence.testsuite.base.cleaner.OracleDatabaseCleaner;
import com.blazebit.persistence.testsuite.base.cleaner.PostgreSQLDatabaseCleaner;
import com.blazebit.persistence.testsuite.base.cleaner.SQLServerDatabaseCleaner;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
/**
*
* @author Christian Beikov
* @since 1.0
*/
public abstract class AbstractJpaPersistenceTest {
private static boolean resolvedNoop = false;
private static Class<?> lastTestClass;
private static Set<Class<?>> databaseCleanerClasses;
private static DatabaseCleaner databaseCleaner;
private static final List<DatabaseCleaner.Factory> DATABASE_CLEANERS = Arrays.asList(
new H2DatabaseCleaner.Factory(),
new PostgreSQLDatabaseCleaner.Factory(),
new DB2DatabaseCleaner.Factory(),
new MySQLDatabaseCleaner.Factory(),
new SQLServerDatabaseCleaner.Factory(),
new OracleDatabaseCleaner.Factory()
);
protected EntityManagerFactory emf;
protected EntityManager em;
protected CriteriaBuilderFactory cbf;
protected JpaProvider jpaProvider;
private boolean schemaChanged;
@BeforeClass
public static void initLogging() {
try {
LogManager.getLogManager().readConfiguration(AbstractJpaPersistenceTest.class.getResourceAsStream(
"/logging.properties"));
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
private DatabaseCleaner getLastDatabaseCleaner() {
if (new HashSet<>(Arrays.asList(getEntityClasses())).equals(databaseCleanerClasses)) {
return databaseCleaner;
}
return null;
}
private void setLastDatabaseCleaner(DatabaseCleaner cleaner) {
databaseCleanerClasses = new HashSet<>(Arrays.asList(getEntityClasses()));
databaseCleaner = cleaner;
}
protected void cleanDatabase() {
// Nothing to delete if the schema changed
if (schemaChanged) {
return;
}
boolean wasAutoCommit = false;
Connection connection = getConnection(em);
try {
// Turn off auto commit if necessary
wasAutoCommit = connection.getAutoCommit();
if (wasAutoCommit) {
connection.setAutoCommit(false);
}
// Clear the data with the cleaner
databaseCleaner.clearData(connection);
} catch (SQLException ex) {
try {
connection.rollback();
} catch (SQLException e1) {
ex.addSuppressed(e1);
}
throw new RuntimeException(ex);
} finally {
if (wasAutoCommit) {
try {
connection.setAutoCommit(true);
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
}
}
private void clearSchema() {
boolean wasAutoCommit = false;
Connection connection = getConnection(em);
try {
// Turn off auto commit if necessary
wasAutoCommit = connection.getAutoCommit();
if (wasAutoCommit) {
connection.setAutoCommit(false);
}
// Clear the data with the cleaner
databaseCleaner.clearSchema(connection);
} catch (SQLException ex) {
try {
connection.rollback();
} catch (SQLException e1) {
ex.addSuppressed(e1);
}
throw new RuntimeException(ex);
} finally {
if (wasAutoCommit) {
try {
connection.setAutoCommit(true);
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
}
}
@Before
public void init() {
boolean firstTest = lastTestClass != getClass();
lastTestClass = getClass();
// If a previous test run resolved the no-op cleaner, we won't be able to resolve any other cleaner
if (resolvedNoop) {
databaseCleaner = null;
schemaChanged = true;
} else {
databaseCleaner = getLastDatabaseCleaner();
schemaChanged = databaseCleaner == null;
}
emf = createEntityManagerFactory("TestsuiteBase", createProperties("none"));
em = emf.createEntityManager();
if (!resolvedNoop && databaseCleaner == null) {
// Find an applicable cleaner
Connection connection = getConnection(em);
DatabaseCleaner applicableCleaner = null;
for (DatabaseCleaner.Factory factory : DATABASE_CLEANERS) {
DatabaseCleaner cleaner = factory.create();
if (cleaner.isApplicable(connection)) {
applicableCleaner = cleaner;
break;
}
}
if (applicableCleaner == null) {
// If none was found, we use the default cleaner
Logger.getLogger(getClass().getName()).warning("Could not resolve database cleaner for the database, falling back to drop-and-create strategy.");
resolvedNoop = true;
}
addIgnores(applicableCleaner);
setLastDatabaseCleaner(applicableCleaner);
}
if (databaseCleaner == null) {
// The default cleaner which recreates the schema
setLastDatabaseCleaner(new DatabaseCleaner() {
@Override
public boolean isApplicable(Connection connection) {
return true;
}
@Override
public boolean supportsClearSchema() {
return false;
}
@Override
public void clearSchema(Connection connection) {
}
@Override
public void addIgnoredTable(String tableName) {
}
@Override
public void clearData(Connection connection) {
recreateOrClearSchema();
}
});
}
CriteriaBuilderConfiguration config = Criteria.getDefault();
config = configure(config);
cbf = config.createCriteriaBuilderFactory(emf);
jpaProvider = cbf.getService(JpaProviderFactory.class).createJpaProvider(em);
if (schemaChanged || !databaseCleaner.supportsClearSchema()) {
recreateOrClearSchema();
setUpOnce();
} else if (firstTest) {
setUpOnce();
}
em.getTransaction().begin();
}
protected void addIgnores(DatabaseCleaner applicableCleaner) {
// No-op
}
protected void createSchema() {
createEntityManagerFactory("TestsuiteBase", createProperties("create")).close();
}
protected void dropSchema() {
createEntityManagerFactory("TestsuiteBase", createProperties("drop")).close();
}
protected void dropAndCreateSchema() {
createEntityManagerFactory("TestsuiteBase", createProperties("drop-and-create")).close();
}
protected void recreateOrClearSchema() {
if (databaseCleaner.supportsClearSchema()) {
clearSchema();
createSchema();
} else {
dropAndCreateSchema();
}
}
protected void setUpOnce() {
// No-op
}
protected abstract boolean supportsMapKeyDeReference();
protected abstract boolean supportsInverseSetCorrelationJoinsSubtypesWhenJoined();
@After
public void destruct() {
EntityManagerFactory factory;
// NOTE: We need to close the entity manager or else we could run into a deadlock on some dbms platforms
// I am looking at you MySQL..
if (em != null && em.isOpen()) {
factory = em.getEntityManagerFactory();
if (em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
em.close();
} else {
factory = emf;
}
if (factory != null && factory.isOpen()) {
factory.close();
}
if (databaseCleaner != null && !databaseCleaner.supportsClearSchema()) {
dropSchema();
}
}
private Properties createProperties(String dbAction) {
Properties properties = new Properties();
properties.put("javax.persistence.jdbc.url", System.getProperty("jdbc.url"));
properties.put("javax.persistence.jdbc.user", System.getProperty("jdbc.user", ""));
properties.put("javax.persistence.jdbc.password", System.getProperty("jdbc.password", ""));
properties.put("javax.persistence.jdbc.driver", System.getProperty("jdbc.driver"));
properties.put("javax.persistence.sharedCache.mode", "NONE");
properties.put("javax.persistence.schema-generation.database.action", dbAction);
properties = applyProperties(properties);
return properties;
}
protected abstract Class<?>[] getEntityClasses();
protected Connection getConnection(EntityManager em) {
return em.unwrap(Connection.class);
}
protected CriteriaBuilderConfiguration configure(CriteriaBuilderConfiguration config) {
return config;
}
protected Properties applyProperties(Properties properties) {
return properties;
}
private EntityManagerFactory createEntityManagerFactory(String persistenceUnitName, Map<Object, Object> properties) {
MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo();
persistenceUnitInfo.setPersistenceUnitName(persistenceUnitName);
persistenceUnitInfo.setTransactionType(PersistenceUnitTransactionType.RESOURCE_LOCAL);
persistenceUnitInfo.setExcludeUnlistedClasses(true);
try {
URL url = AbstractJpaPersistenceTest.class.getClassLoader()
.getResource("");
persistenceUnitInfo.setPersistenceUnitRootUrl(url);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
for (Class<?> clazz : getEntityClasses()) {
persistenceUnitInfo.addManagedClassName(clazz.getName());
}
return createEntityManagerFactory(persistenceUnitInfo, properties);
}
private static EntityManagerFactory createEntityManagerFactory(PersistenceUnitInfo persistenceUnitInfo, Map<Object, Object> properties) {
EntityManagerFactory factory = null;
Map<Object, Object> props = properties;
if (props == null) {
props = Collections.emptyMap();
}
PersistenceProviderResolver resolver = PersistenceProviderResolverHolder.getPersistenceProviderResolver();
List<PersistenceProvider> providers = resolver.getPersistenceProviders();
Map<String, Throwable> exceptions = new HashMap<String, Throwable>();
StringBuffer foundProviders = null;
for (PersistenceProvider provider : providers) {
String providerName = provider.getClass()
.getName();
try {
factory = provider.createContainerEntityManagerFactory(persistenceUnitInfo, props);
} catch (Exception e) {
// capture the exception details and give other providers a chance
exceptions.put(providerName, e);
}
if (factory != null) {
// we're done
return factory;
} else {
// update the list of providers we have tried
if (foundProviders == null) {
foundProviders = new StringBuffer(providerName);
} else {
foundProviders.append(", ");
foundProviders.append(providerName);
}
}
}
// make sure our providers list is initialized for the exceptions below
if (foundProviders == null) {
foundProviders = new StringBuffer("NONE");
}
if (exceptions.isEmpty()) {
// throw an exception with the PU name and providers we tried
throw new PersistenceException("No persistence providers available for \"" + persistenceUnitInfo
.getPersistenceUnitName() + "\" after trying the following discovered implementations: " + foundProviders);
} else {
// we encountered one or more exceptions, so format and throw as a single exception
throw createPersistenceException(
"Explicit persistence provider error(s) occurred for \"" + persistenceUnitInfo.getPersistenceUnitName()
+ "\" after trying the following discovered implementations: " + foundProviders,
exceptions);
}
}
private static PersistenceException createPersistenceException(String msg, Map<String, Throwable> failures) {
String newline = System.getProperty("line.separator");
StringWriter strWriter = new StringWriter();
strWriter.append(msg);
if (failures.size() <= 1) {
// we caught an exception, so include it as the cause
Throwable t = null;
for (String providerName : failures.keySet()) {
t = failures.get(providerName);
strWriter.append(" from provider: ");
strWriter.append(providerName);
break;
}
return new PersistenceException(strWriter.toString(), t);
} else {
// we caught multiple exceptions, so format them into the message string and don't set a cause
strWriter.append(" with the following failures:");
strWriter.append(newline);
for (String providerName : failures.keySet()) {
strWriter.append(providerName);
strWriter.append(" returned: ");
failures.get(providerName)
.printStackTrace(new PrintWriter(strWriter));
}
strWriter.append(newline);
return new PersistenceException(strWriter.toString());
}
}
}