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;
041
042public abstract class DatabaseHelper {
043
044    private static final Log log = LogFactory.getLog(DatabaseHelper.class);
045
046    public static final String DB_PROPERTY = "nuxeo.test.vcs.db";
047
048    public static final String DB_DEFAULT = "H2";
049
050    public static final String DEF_ID_TYPE = "varchar"; // "varchar", "uuid", "sequence"
051
052    public static DatabaseHelper DATABASE;
053
054    public static final String DB_CLASS_NAME_BASE = "org.nuxeo.ecm.core.storage.sql.Database";
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    // available for JDBC tests
068    public static final String DRIVER_PROPERTY = "nuxeo.test.vcs.driver";
069
070    // available for JDBC tests
071    public static final String URL_PROPERTY = "nuxeo.test.vcs.url";
072
073    public static final String SERVER_PROPERTY = "nuxeo.test.vcs.server";
074
075    public static final String PORT_PROPERTY = "nuxeo.test.vcs.port";
076
077    public static final String DATABASE_PROPERTY = "nuxeo.test.vcs.database";
078
079    public static final String USER_PROPERTY = "nuxeo.test.vcs.user";
080
081    public static final String PASSWORD_PROPERTY = "nuxeo.test.vcs.password";
082
083    public static final String ID_TYPE_PROPERTY = "nuxeo.test.vcs.idtype";
084
085    protected Error owner;
086
087    public static String setSystemProperty(String name, String def) {
088        String value = System.getProperty(name);
089        if (value == null || value.equals("") || value.equals("${" + name + "}")) {
090            System.setProperty(name, def);
091        }
092        return value;
093    }
094
095    public static String setProperty(String name, String def) {
096        String value = System.getProperty(name);
097        if (value == null || value.equals("") || value.equals("${" + name + "}")) {
098            value = def;
099        }
100        Framework.getProperties().setProperty(name, value);
101        return value;
102    }
103
104    public static final String DEFAULT_DATABASE_NAME = "nuxeojunittests";
105
106    public String databaseName = DEFAULT_DATABASE_NAME;
107
108    public void setDatabaseName(String name) {
109        databaseName = name;
110    }
111
112    /**
113     * Sets the database backend used for VCS unit tests.
114     */
115    public static void setDatabaseForTests(String className) {
116        try {
117            DATABASE = (DatabaseHelper) Class.forName(className).newInstance();
118        } catch (ReflectiveOperationException e) {
119            throw new ExceptionInInitializerError("Database class not found: " + className);
120        }
121    }
122
123    /**
124     * Gets a database connection, retrying if the server says it's overloaded.
125     *
126     * @since 5.9.3
127     */
128    public static Connection getConnection(String url, String user, String password) throws SQLException {
129        return JDBCUtils.getConnection(url, user, password);
130    }
131
132    /**
133     * Executes one statement on all the tables in a database.
134     */
135    public static void doOnAllTables(Connection connection, String catalog, String schemaPattern, String statement)
136            throws SQLException {
137        DatabaseMetaData metadata = connection.getMetaData();
138        List<String> tableNames = new LinkedList<String>();
139        Set<String> truncateFirst = new HashSet<String>();
140        ResultSet rs = metadata.getTables(catalog, schemaPattern, "%", new String[] { "TABLE" });
141        while (rs.next()) {
142            String tableName = rs.getString("TABLE_NAME");
143            if (tableName.indexOf('$') != -1) {
144                // skip Oracle 10g flashback/fulltext-index tables
145                continue;
146            }
147            if (tableName.toLowerCase().startsWith("trace_xe_")) {
148                // Skip mssql 2012 system table
149                continue;
150            }
151            if ("ACLR_USER_USERS".equals(tableName)) {
152                // skip nested table that is dropped by the main table
153                continue;
154            }
155            if ("ANCESTORS_ANCESTORS".equals(tableName)) {
156                // skip nested table that is dropped by the main table
157                continue;
158            }
159            if ("ACLR_MODIFIED".equals(tableName) && DATABASE instanceof DatabaseOracle) {
160                // global temporary table on Oracle, must TRUNCATE before DROP
161                truncateFirst.add(tableName);
162            }
163            tableNames.add(tableName);
164        }
165        // not all databases can cascade on drop
166        // remove hierarchy last because of foreign keys
167        if (tableNames.remove("HIERARCHY")) {
168            tableNames.add("HIERARCHY");
169        }
170        // needed for Azure
171        if (tableNames.remove("NXP_LOGS")) {
172            tableNames.add("NXP_LOGS");
173        }
174        if (tableNames.remove("NXP_LOGS_EXTINFO")) {
175            tableNames.add("NXP_LOGS_EXTINFO");
176        }
177        // PostgreSQL is lowercase
178        if (tableNames.remove("hierarchy")) {
179            tableNames.add("hierarchy");
180        }
181        Statement st = connection.createStatement();
182        for (String tableName : tableNames) {
183            if (truncateFirst.contains(tableName)) {
184                String sql = String.format("TRUNCATE TABLE \"%s\"", tableName);
185                executeSql(st, sql);
186            }
187            String sql = String.format(statement, tableName);
188            executeSql(st, sql);
189        }
190        st.close();
191    }
192
193    protected static void executeSql(Statement st, String sql) throws SQLException {
194        log.trace("SQL: " + sql);
195        st.execute(sql);
196    }
197
198    public void setUp() throws SQLException {
199        setOwner();
200        setDatabaseName(DEFAULT_DATABASE_NAME);
201        setBinaryManager(defaultBinaryManager, "");
202        Framework.addListener(new RuntimeServiceListener() {
203
204            @Override
205            public void handleEvent(RuntimeServiceEvent event) {
206                if (RuntimeServiceEvent.RUNTIME_STOPPED == event.id) {
207                    try {
208                        tearDown();
209                    } catch (SQLException cause) {
210                        throw new AssertionError("Cannot teardown database", cause);
211                    }
212                }
213            }
214        });
215    }
216
217    protected void setOwner() {
218        if (owner != null) {
219            Error e = new Error("Second call to setUp() without tearDown()", owner);
220            log.fatal(e.getMessage(), e);
221            throw e;
222        }
223        owner = new Error("Database not released");
224    }
225
226    /**
227     * @throws SQLException
228     */
229    public void tearDown() throws SQLException {
230        owner = null;
231    }
232
233    public static void setBinaryManager(Class<? extends BinaryManager> binaryManagerClass, String key) {
234        setProperty("nuxeo.test.vcs.binary-manager", binaryManagerClass.getName());
235        setProperty("nuxeo.test.vcs.binary-manager-key", key);
236    }
237
238    public abstract String getDeploymentContrib();
239
240    public abstract RepositoryDescriptor getRepositoryDescriptor();
241
242    /**
243     * For databases that do asynchronous fulltext indexing, sleep a bit.
244     */
245    public void sleepForFulltext() {
246    }
247
248    /**
249     * For databases that fail to cascade deletes beyond a certain depth.
250     */
251    public int getRecursiveRemovalDepthLimit() {
252        return 0;
253    }
254
255    /**
256     * For databases that don't support clustering.
257     */
258    public boolean supportsClustering() {
259        return false;
260    }
261
262    public boolean supportsMultipleFulltextIndexes() {
263        return true;
264    }
265
266    public boolean supportsXA() {
267        return true;
268    }
269
270    public boolean supportsSoftDelete() {
271        return false;
272    }
273
274    /**
275     * Whether this database supports "sequence" as an id type.
276     *
277     * @since 5.9.3
278     */
279    public boolean supportsSequenceId() {
280        return false;
281    }
282
283    public boolean supportsArrayColumns() {
284        return false;
285    }
286
287}