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}