001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 * Benoit Delbosc 012 */ 013 014package org.nuxeo.ecm.core.storage.sql; 015 016import java.sql.Connection; 017import java.sql.DatabaseMetaData; 018import java.sql.ResultSet; 019import java.sql.SQLException; 020import java.sql.Statement; 021import java.util.HashSet; 022import java.util.LinkedList; 023import java.util.List; 024import java.util.Set; 025 026import org.apache.commons.logging.Log; 027import org.apache.commons.logging.LogFactory; 028import org.nuxeo.common.utils.JDBCUtils; 029import org.nuxeo.ecm.core.blob.binary.BinaryManager; 030import org.nuxeo.ecm.core.blob.binary.DefaultBinaryManager; 031import org.nuxeo.ecm.core.repository.RepositoryFactory; 032import org.nuxeo.ecm.core.storage.sql.coremodel.SQLRepositoryFactory; 033import org.nuxeo.runtime.RuntimeServiceEvent; 034import org.nuxeo.runtime.RuntimeServiceListener; 035import org.nuxeo.runtime.api.Framework; 036import org.nuxeo.runtime.datasource.ConnectionHelper; 037 038public abstract class DatabaseHelper { 039 040 private static final Log log = LogFactory.getLog(DatabaseHelper.class); 041 042 public static final String DB_PROPERTY = "nuxeo.test.vcs.db"; 043 044 public static final String DB_DEFAULT = "H2"; 045 046 public static final String DEF_ID_TYPE = "varchar"; // "varchar", "uuid", "sequence" 047 048 private static final boolean SINGLEDS_DEFAULT = false; 049 050 public static DatabaseHelper DATABASE; 051 052 public static final String DB_CLASS_NAME_BASE = "org.nuxeo.ecm.core.storage.sql.Database"; 053 054 protected static final Class<? extends RepositoryFactory> defaultRepositoryFactory = SQLRepositoryFactory.class; 055 056 protected static final Class<? extends BinaryManager> defaultBinaryManager = DefaultBinaryManager.class; 057 058 static { 059 setSystemProperty(DB_PROPERTY, DB_DEFAULT); 060 String className = System.getProperty(DB_PROPERTY); 061 if (className.indexOf('.') < 0) { 062 className = DB_CLASS_NAME_BASE + className; 063 } 064 setDatabaseForTests(className); 065 } 066 067 public static final String REPOSITORY_PROPERTY = "nuxeo.test.vcs.repository"; 068 069 // available for JDBC tests 070 public static final String DRIVER_PROPERTY = "nuxeo.test.vcs.driver"; 071 072 // available for JDBC tests 073 public static final String XA_DATASOURCE_PROPERTY = "nuxeo.test.vcs.xadatasource"; 074 075 // available for JDBC tests 076 public static final String URL_PROPERTY = "nuxeo.test.vcs.url"; 077 078 public static final String SERVER_PROPERTY = "nuxeo.test.vcs.server"; 079 080 public static final String PORT_PROPERTY = "nuxeo.test.vcs.port"; 081 082 public static final String DATABASE_PROPERTY = "nuxeo.test.vcs.database"; 083 084 public static final String USER_PROPERTY = "nuxeo.test.vcs.user"; 085 086 public static final String PASSWORD_PROPERTY = "nuxeo.test.vcs.password"; 087 088 public static final String ID_TYPE_PROPERTY = "nuxeo.test.vcs.idtype"; 089 090 // set this to true to activate single datasource for all tests 091 public static final String SINGLEDS_PROPERTY = "nuxeo.test.vcs.singleds"; 092 093 protected Error owner; 094 095 public static String setSystemProperty(String name, String def) { 096 String value = System.getProperty(name); 097 if (value == null || value.equals("") || value.equals("${" + name + "}")) { 098 System.setProperty(name, def); 099 } 100 return value; 101 } 102 103 public static String setProperty(String name, String def) { 104 String value = System.getProperty(name); 105 if (value == null || value.equals("") || value.equals("${" + name + "}")) { 106 value = def; 107 } 108 Framework.getProperties().put(name, value); 109 return value; 110 } 111 112 public static final String DEFAULT_DATABASE_NAME = "nuxeojunittests"; 113 114 public String databaseName = DEFAULT_DATABASE_NAME; 115 116 public void setDatabaseName(String name) { 117 databaseName = name; 118 } 119 120 public static final String DEFAULT_REPOSITORY_NAME = "test"; 121 122 public String repositoryName = DEFAULT_REPOSITORY_NAME; 123 124 /** 125 * Sets the database backend used for VCS unit tests. 126 */ 127 public static void setDatabaseForTests(String className) { 128 try { 129 DATABASE = (DatabaseHelper) Class.forName(className).newInstance(); 130 } catch (Exception e) { 131 throw new ExceptionInInitializerError("Database class not found: " + className); 132 } 133 String msg = "Database used for VCS tests: " + className; 134 // System.out used on purpose, don't remove 135 System.out.println(DatabaseHelper.class.getSimpleName() + ": " + msg); 136 log.info(msg); 137 } 138 139 /** 140 * Gets a database connection, retrying if the server says it's overloaded. 141 * 142 * @since 5.9.3 143 */ 144 public static Connection getConnection(String url, String user, String password) throws SQLException { 145 return JDBCUtils.getConnection(url, user, password); 146 } 147 148 /** 149 * Executes one statement on all the tables in a database. 150 */ 151 public static void doOnAllTables(Connection connection, String catalog, String schemaPattern, String statement) 152 throws SQLException { 153 DatabaseMetaData metadata = connection.getMetaData(); 154 List<String> tableNames = new LinkedList<String>(); 155 Set<String> truncateFirst = new HashSet<String>(); 156 ResultSet rs = metadata.getTables(catalog, schemaPattern, "%", new String[] { "TABLE" }); 157 while (rs.next()) { 158 String tableName = rs.getString("TABLE_NAME"); 159 if (tableName.indexOf('$') != -1) { 160 // skip Oracle 10g flashback/fulltext-index tables 161 continue; 162 } 163 if (tableName.toLowerCase().startsWith("trace_xe_")) { 164 // Skip mssql 2012 system table 165 continue; 166 } 167 if ("ACLR_USER_USERS".equals(tableName)) { 168 // skip nested table that is dropped by the main table 169 continue; 170 } 171 if ("ANCESTORS_ANCESTORS".equals(tableName)) { 172 // skip nested table that is dropped by the main table 173 continue; 174 } 175 if ("ACLR_MODIFIED".equals(tableName) && DATABASE instanceof DatabaseOracle) { 176 // global temporary table on Oracle, must TRUNCATE before DROP 177 truncateFirst.add(tableName); 178 } 179 tableNames.add(tableName); 180 } 181 // not all databases can cascade on drop 182 // remove hierarchy last because of foreign keys 183 if (tableNames.remove("HIERARCHY")) { 184 tableNames.add("HIERARCHY"); 185 } 186 // needed for Azure 187 if (tableNames.remove("NXP_LOGS")) { 188 tableNames.add("NXP_LOGS"); 189 } 190 if (tableNames.remove("NXP_LOGS_EXTINFO")) { 191 tableNames.add("NXP_LOGS_EXTINFO"); 192 } 193 // PostgreSQL is lowercase 194 if (tableNames.remove("hierarchy")) { 195 tableNames.add("hierarchy"); 196 } 197 Statement st = connection.createStatement(); 198 for (String tableName : tableNames) { 199 if (truncateFirst.contains(tableName)) { 200 String sql = String.format("TRUNCATE TABLE \"%s\"", tableName); 201 executeSql(st, sql); 202 } 203 String sql = String.format(statement, tableName); 204 executeSql(st, sql); 205 } 206 st.close(); 207 } 208 209 protected static void executeSql(Statement st, String sql) throws SQLException { 210 log.trace("SQL: " + sql); 211 st.execute(sql); 212 } 213 214 public void setUp(Class<? extends RepositoryFactory> factoryClass) throws Exception { 215 setUp(); 216 setRepositoryFactory(factoryClass); 217 } 218 219 public void setUp() throws Exception { 220 setOwner(); 221 setDatabaseName(DEFAULT_DATABASE_NAME); 222 setRepositoryFactory(defaultRepositoryFactory); 223 setBinaryManager(defaultBinaryManager, ""); 224 setSingleDataSourceMode(); 225 Framework.addListener(new RuntimeServiceListener() { 226 227 @Override 228 public void handleEvent(RuntimeServiceEvent event) { 229 if (RuntimeServiceEvent.RUNTIME_STOPPED == event.id) { 230 try { 231 tearDown(); 232 } catch (SQLException cause) { 233 throw new AssertionError("Cannot teardown database", cause); 234 } 235 } 236 } 237 }); 238 } 239 240 protected void setOwner() { 241 if (owner != null) { 242 Error e = new Error("Second call to setUp() without tearDown()", owner); 243 log.fatal(e.getMessage(), e); 244 throw e; 245 } 246 owner = new Error("Database not released"); 247 } 248 249 /** 250 * @throws SQLException 251 */ 252 public void tearDown() throws SQLException { 253 owner = null; 254 } 255 256 public static void setRepositoryFactory(Class<? extends RepositoryFactory> factoryClass) { 257 setProperty("nuxeo.test.vcs.repository-factory", factoryClass.getName()); 258 } 259 260 public static void setBinaryManager(Class<? extends BinaryManager> binaryManagerClass, String key) { 261 setProperty("nuxeo.test.vcs.binary-manager", binaryManagerClass.getName()); 262 setProperty("nuxeo.test.vcs.binary-manager-key", key); 263 } 264 265 public abstract String getDeploymentContrib(); 266 267 public abstract RepositoryDescriptor getRepositoryDescriptor(); 268 269 public static void setSingleDataSourceMode() { 270 if (Boolean.parseBoolean(System.getProperty(SINGLEDS_PROPERTY)) || SINGLEDS_DEFAULT) { 271 // the name doesn't actually matter, as code in 272 // ConnectionHelper.getDataSource ignores it and uses 273 // nuxeo.test.vcs.url etc. for connections in test mode 274 String dataSourceName = "jdbc/NuxeoTestDS"; 275 Framework.getProperties().setProperty(ConnectionHelper.SINGLE_DS, dataSourceName); 276 } 277 } 278 279 /** 280 * For databases that do asynchronous fulltext indexing, sleep a bit. 281 */ 282 public void sleepForFulltext() { 283 } 284 285 /** 286 * For databases that don't have subsecond resolution, sleep a bit to get to the next second. 287 */ 288 public void maybeSleepToNextSecond() { 289 if (!hasSubSecondResolution()) { 290 try { 291 Thread.sleep(1000); 292 } catch (InterruptedException e) { 293 } 294 } 295 } 296 297 /** 298 * For databases that don't have subsecond resolution, like MySQL. 299 */ 300 public boolean hasSubSecondResolution() { 301 return true; 302 } 303 304 /** 305 * For databases that fail to cascade deletes beyond a certain depth. 306 */ 307 public int getRecursiveRemovalDepthLimit() { 308 return 0; 309 } 310 311 /** 312 * For databases that don't support clustering. 313 */ 314 public boolean supportsClustering() { 315 return false; 316 } 317 318 public boolean supportsMultipleFulltextIndexes() { 319 return true; 320 } 321 322 public boolean supportsXA() { 323 return true; 324 } 325 326 public boolean supportsSoftDelete() { 327 return false; 328 } 329 330 /** 331 * Whether this database supports "sequence" as an id type. 332 * 333 * @since 5.9.3 334 */ 335 public boolean supportsSequenceId() { 336 return false; 337 } 338 339 public boolean supportsArrayColumns() { 340 return false; 341 } 342 343}