/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Florent Guillaume * Benoit Delbosc */ package org.nuxeo.ecm.core.storage.sql; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.LinkedList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.repository.RepositoryFactory; import org.nuxeo.ecm.core.storage.binary.BinaryManager; import org.nuxeo.ecm.core.storage.binary.DefaultBinaryManager; import org.nuxeo.ecm.core.storage.sql.coremodel.SQLRepositoryFactory; import org.nuxeo.runtime.RuntimeServiceEvent; import org.nuxeo.runtime.RuntimeServiceListener; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.datasource.ConnectionHelper; public abstract class DatabaseHelper { private static final Log log = LogFactory.getLog(DatabaseHelper.class); public static final String DB_PROPERTY = "nuxeo.test.vcs.db"; public static final String DB_DEFAULT = "H2"; public static final String DEF_ID_TYPE = "varchar"; // "varchar", "uuid", "sequence" private static final boolean SINGLEDS_DEFAULT = false; public static DatabaseHelper DATABASE; public static final String DB_CLASS_NAME_BASE = "org.nuxeo.ecm.core.storage.sql.Database"; /** * Maximum number of times we retry a connection if the server says it's * overloaded. */ private static final int MAX_CONNECTION_TRIES = 5; protected static final Class<? extends RepositoryFactory> defaultRepositoryFactory = SQLRepositoryFactory.class; protected static final Class<? extends BinaryManager> defaultBinaryManager = DefaultBinaryManager.class; static { setSystemProperty(DB_PROPERTY, DB_DEFAULT); String className = System.getProperty(DB_PROPERTY); if (className.indexOf('.') < 0) { className = DB_CLASS_NAME_BASE + className; } setDatabaseForTests(className); } public static final String REPOSITORY_PROPERTY = "nuxeo.test.vcs.repository"; // available for JDBC tests public static final String DRIVER_PROPERTY = "nuxeo.test.vcs.driver"; // available for JDBC tests public static final String XA_DATASOURCE_PROPERTY = "nuxeo.test.vcs.xadatasource"; // available for JDBC tests public static final String URL_PROPERTY = "nuxeo.test.vcs.url"; public static final String SERVER_PROPERTY = "nuxeo.test.vcs.server"; public static final String PORT_PROPERTY = "nuxeo.test.vcs.port"; public static final String DATABASE_PROPERTY = "nuxeo.test.vcs.database"; public static final String USER_PROPERTY = "nuxeo.test.vcs.user"; public static final String PASSWORD_PROPERTY = "nuxeo.test.vcs.password"; public static final String ID_TYPE_PROPERTY = "nuxeo.test.vcs.idtype"; // set this to true to activate single datasource for all tests public static final String SINGLEDS_PROPERTY = "nuxeo.test.vcs.singleds"; protected Error owner; public static String setSystemProperty(String name, String def) { String value = System.getProperty(name); if (value == null || value.equals("") || value.equals("${" + name + "}")) { System.setProperty(name, def); } return value; } public static String setProperty(String name, String def) { String value = System.getProperty(name); if (value == null || value.equals("") || value.equals("${" + name + "}")) { value = def; } Framework.getProperties().put(name, value); return value; } public static final String DEFAULT_DATABASE_NAME = "nuxeojunittests"; public String databaseName = DEFAULT_DATABASE_NAME; public void setDatabaseName(String name) { databaseName = name; } public static final String DEFAULT_REPOSITORY_NAME = "test"; public String repositoryName = DEFAULT_REPOSITORY_NAME; public void setRepositoryName(String name) { repositoryName = name; } /** * Sets the database backend used for VCS unit tests. */ public static void setDatabaseForTests(String className) { try { DATABASE = (DatabaseHelper) Class.forName(className).newInstance(); } catch (Exception e) { throw new ExceptionInInitializerError("Database class not found: " + className); } String msg = "Database used for VCS tests: " + className; // System.out used on purpose, don't remove System.out.println(DatabaseHelper.class.getSimpleName() + ": " + msg); log.info(msg); } /** * Gets a database connection, retrying if the server says it's overloaded. * * @since 5.9.3 */ public static Connection getConnection(String url, String user, String password) throws SQLException { for (int tryNo = 1;; tryNo++) { try { return DriverManager.getConnection(url, user, password); } catch (SQLException e) { if (tryNo >= MAX_CONNECTION_TRIES) { throw e; } if (e.getErrorCode() != 12519) { throw e; } // Oracle: Listener refused the connection with the // following error: ORA-12519, TNS:no appropriate // service handler found // SQLState = "66000" // Happens when connections are open too fast (unit tests) // -> retry a few times after a small delay log.warn(String.format( "Connections open too fast, retrying in %ds: %s", tryNo, e.getMessage().replace("\n", " "))); try { Thread.sleep(1000 * tryNo); } catch (InterruptedException ie) { // restore interrupted status Thread.currentThread().interrupt(); throw new RuntimeException("interrupted"); } } } } /** * Executes one statement on all the tables in a database. */ public static void doOnAllTables(Connection connection, String catalog, String schemaPattern, String statement) throws SQLException { DatabaseMetaData metadata = connection.getMetaData(); List<String> tableNames = new LinkedList<String>(); ResultSet rs = metadata.getTables(catalog, schemaPattern, "%", new String[] { "TABLE" }); while (rs.next()) { String tableName = rs.getString("TABLE_NAME"); if (tableName.indexOf('$') != -1) { // skip Oracle 10g flashback/fulltext-index tables continue; } if (tableName.toLowerCase().startsWith("trace_xe_")) { // Skip mssql 2012 system table continue; } if ("ACLR_USER_USERS".equals(tableName)) { // skip nested table that is dropped by the main table continue; } if ("ANCESTORS_ANCESTORS".equals(tableName)) { // skip nested table that is dropped by the main table continue; } if ("ACLR_MODIFIED".equals(tableName) && DATABASE instanceof DatabaseOracle) { // skip temporary table on Oracle, cannot be dropped continue; } tableNames.add(tableName); } // not all databases can cascade on drop // remove hierarchy last because of foreign keys if (tableNames.remove("HIERARCHY")) { tableNames.add("HIERARCHY"); } // needed for Azure if (tableNames.remove("NXP_LOGS")) { tableNames.add("NXP_LOGS"); } if (tableNames.remove("NXP_LOGS_EXTINFO")) { tableNames.add("NXP_LOGS_EXTINFO"); } // PostgreSQL is lowercase if (tableNames.remove("hierarchy")) { tableNames.add("hierarchy"); } Statement st = connection.createStatement(); for (String tableName : tableNames) { String sql = String.format(statement, tableName); executeSql(st, sql); } st.close(); } protected static void executeSql(Statement st, String sql) throws SQLException { log.trace("SQL: " + sql); st.execute(sql); } public void setUp(Class<? extends RepositoryFactory> factoryClass) throws Exception { setUp(); setRepositoryFactory(factoryClass); } public void setUp() throws Exception { setOwner(); setDatabaseName(DEFAULT_DATABASE_NAME); setRepositoryName(DEFAULT_REPOSITORY_NAME); setRepositoryFactory(defaultRepositoryFactory); setBinaryManager(defaultBinaryManager, ""); setSingleDataSourceMode(); Framework.addListener(new RuntimeServiceListener() { @Override public void handleEvent(RuntimeServiceEvent event) { if (RuntimeServiceEvent.RUNTIME_STOPPED == event.id) { try { tearDown(); } catch (SQLException cause) { throw new AssertionError("Cannot teardown database", cause); } } } }); } protected void setOwner() { if (owner != null) { Error e = new Error("Second call to setUp() without tearDown()", owner); log.fatal(e.getMessage(), e); throw e; } owner = new Error("Database not released"); } /** * @throws SQLException */ public void tearDown() throws SQLException { owner = null; } public static void setRepositoryFactory( Class<? extends RepositoryFactory> factoryClass) { setProperty("nuxeo.test.vcs.repository-factory", factoryClass.getName()); } public static void setBinaryManager( Class<? extends BinaryManager> binaryManagerClass, String key) { setProperty("nuxeo.test.vcs.binary-manager", binaryManagerClass.getName()); setProperty("nuxeo.test.vcs.binary-manager-key", key); } public abstract String getDeploymentContrib(); public abstract RepositoryDescriptor getRepositoryDescriptor(); public static void setSingleDataSourceMode() { if (Boolean.parseBoolean(System.getProperty(SINGLEDS_PROPERTY)) || SINGLEDS_DEFAULT) { // the name doesn't actually matter, as code in // ConnectionHelper.getDataSource ignores it and uses // nuxeo.test.vcs.url etc. for connections in test mode String dataSourceName = "jdbc/NuxeoTestDS"; Framework.getProperties().setProperty(ConnectionHelper.SINGLE_DS, dataSourceName); } } /** * For databases that do asynchronous fulltext indexing, sleep a bit. */ public void sleepForFulltext() { } /** * For databases that don't have subsecond resolution, sleep a bit to get to * the next second. */ public void maybeSleepToNextSecond() { if (!hasSubSecondResolution()) { try { Thread.sleep(1000); } catch (InterruptedException e) { } } } /** * For databases that don't have subsecond resolution, like MySQL. */ public boolean hasSubSecondResolution() { return true; } /** * For databases that fail to cascade deletes beyond a certain depth. */ public int getRecursiveRemovalDepthLimit() { return 0; } /** * For databases that don't support clustering. */ public boolean supportsClustering() { return false; } public boolean supportsMultipleFulltextIndexes() { return true; } public boolean supportsXA() { return true; } public boolean supportsSoftDelete() { return false; } /** * Whether this database supports "sequence" as an id type. * * @since 5.9.3 */ public boolean supportsSequenceId() { return false; } public boolean supportsArrayColumns() { return false; } }