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