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}