001package fr.ifremer.adagio.core.test;
002
003/*
004 * #%L
005 * SIH-Adagio Core for Allegro
006 * $Id: DatabaseResource.java 607 2013-04-21 12:34:44Z tc1fbb1 $
007 * $HeadURL: https://forge.ifremer.fr/svn/sih-adagio/trunk/adagio/core-allegro/src/test/java/fr/ifremer/adagio/core/DatabaseResource.java $
008 * %%
009 * Copyright (C) 2012 - 2013 Ifremer
010 * %%
011 * This program is free software: you can redistribute it and/or modify
012 * it under the terms of the GNU Affero General Public License as published by
013 * the Free Software Foundation, either version 3 of the License, or
014 * (at your option) any later version.
015 * 
016 * This program is distributed in the hope that it will be useful,
017 * but WITHOUT ANY WARRANTY; without even the implied warranty of
018 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
019 * GNU General Public License for more details.
020 * 
021 * You should have received a copy of the GNU Affero General Public License
022 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
023 * #L%
024 */
025
026import java.io.BufferedReader;
027import java.io.BufferedWriter;
028import java.io.File;
029import java.io.IOException;
030import java.io.InputStream;
031import java.sql.Connection;
032import java.sql.PreparedStatement;
033import java.sql.SQLException;
034import java.util.List;
035import java.util.Locale;
036import java.util.Properties;
037import java.util.Set;
038
039import javax.sql.DataSource;
040
041import org.apache.commons.io.FileUtils;
042import org.apache.commons.lang3.StringUtils;
043import org.apache.commons.logging.Log;
044import org.apache.commons.logging.LogFactory;
045import org.junit.Assume;
046import org.junit.rules.TestRule;
047import org.junit.runner.Description;
048import org.junit.runners.model.Statement;
049import org.nuiton.i18n.I18n;
050import org.nuiton.i18n.init.DefaultI18nInitializer;
051import org.nuiton.i18n.init.UserI18nInitializer;
052
053import com.google.common.base.Charsets;
054import com.google.common.base.Preconditions;
055import com.google.common.base.Predicate;
056import com.google.common.collect.Lists;
057import com.google.common.collect.Sets;
058import com.google.common.io.Files;
059
060import fr.ifremer.adagio.core.config.AdagioConfiguration;
061import fr.ifremer.adagio.core.config.AdagioConfigurationOption;
062import fr.ifremer.adagio.core.dao.technical.DaoUtils;
063import fr.ifremer.adagio.core.service.ServiceLocator;
064
065/**
066 * To be able to manage database connection for unit test.
067 * 
068 * @author blavenie <benoit.lavenier@e-is.pro>
069 * @since 3.3.3
070 */
071public abstract class DatabaseResource implements TestRule {
072
073    /** Logger. */
074    protected static final Log log = LogFactory.getLog(DatabaseResource.class);
075
076    public static final String BUILD_ENVIRONMENT_DEFAULT = "hsqldb";
077    public static final String HSQLDB_SRC_DATABASE_DIRECTORY = "src/test/db";
078    public static final String HSQLDB_SRC_DATABASE_CREATE_SCRIPT = HSQLDB_SRC_DATABASE_DIRECTORY + "/allegro.script";
079
080    public static long BUILD_TIMESTAMP = System.nanoTime();
081
082    private File resourceDirectory;
083    
084    private String dbDirectory;
085
086    protected final String beanFactoryReferenceLocation;
087
088    protected final String beanRefFactoryReferenceId;
089
090    private final boolean writeDb;
091    
092    private String configName;
093    
094    private boolean witherror = false;
095
096    protected Class<?> testClass;
097
098    protected DatabaseResource(String configName, String beanFactoryReferenceLocation,
099                                String beanRefFactoryReferenceId,
100                                boolean writeDb) {
101        this.configName = configName;
102        this.beanFactoryReferenceLocation = beanFactoryReferenceLocation;
103        this.beanRefFactoryReferenceId = beanRefFactoryReferenceId;
104        this.writeDb = writeDb;
105    }
106    
107    /**
108     * Return configuration files prefix (i.e. 'allegro-test')
109     * Could be override by external project
110     * @return the prefix to use to retrieve configuration files 
111     */
112    protected abstract String getConfigFilesPrefix();
113
114    public File getResourceDirectory(String name) {
115        return new File(resourceDirectory, name);
116    }
117
118    public File getResourceDirectory() {
119        return resourceDirectory;
120    }
121    
122    protected boolean isWriteDb() {
123        return writeDb;
124    }
125
126    @Override
127    public Statement apply(final Statement base, final Description description) {
128
129        return new Statement() {
130            @Override
131            public void evaluate() throws Throwable {
132                before(description);
133                try {
134                    base.evaluate();
135                } catch (Throwable e) {
136                    witherror = true;
137                } finally {
138                    after(description);
139                }
140            }
141        };
142    }
143
144    protected void before(Description description) throws Throwable {
145        testClass = description.getTestClass();
146
147        boolean prepareDb = beanFactoryReferenceLocation == null || !beanFactoryReferenceLocation.contains("WithNoDb");
148        boolean defaultDbName = StringUtils.isEmpty(configName);
149
150        dbDirectory = null;
151        if (defaultDbName) {
152            configName = "db";
153        }
154
155        if (log.isInfoEnabled()) {
156            log.info("Prepare test " + testClass);
157        }
158
159        resourceDirectory = getTestSpecificDirectory(testClass, "");
160
161        // Load building env
162        String buildEnvironment = getBuildEnvironment();
163
164        // check that config file is in classpath (avoid to find out why it does not works...)
165        String configFilename = getConfigFilesPrefix();
166        if (prepareDb) {
167            configFilename += "-" + (writeDb ? "write" : "read");
168        }
169        if (!defaultDbName) {
170            configFilename += "-" + configName;
171        }
172        String configFilenameNoEnv = configFilename + ".properties";
173        if (StringUtils.isNotBlank(buildEnvironment)) {
174            configFilename += "-" + buildEnvironment;
175        }
176        configFilename += ".properties";
177        
178        InputStream resourceAsStream = getClass().getResourceAsStream("/" + configFilename);
179        if (resourceAsStream == null && StringUtils.isNotBlank(buildEnvironment)) {
180            resourceAsStream = getClass().getResourceAsStream("/" + configFilenameNoEnv);
181            Preconditions.checkNotNull(resourceAsStream, "Could not find " + configFilename + " or "+ configFilenameNoEnv +" in test class-path");
182            configFilename = configFilenameNoEnv;
183        }
184        else {
185            Preconditions.checkNotNull(resourceAsStream, "Could not find " + configFilename + " in test class-path");
186        }
187
188        // Prepare DB
189        if (prepareDb && "hsqldb".equalsIgnoreCase(buildEnvironment)) {
190                    
191            dbDirectory = HSQLDB_SRC_DATABASE_DIRECTORY;
192            if (!defaultDbName) {
193                dbDirectory += configName;
194            }
195            TestUtil.checkDbExists(testClass, dbDirectory);
196
197            if (writeDb) {
198                Properties p = new Properties();
199                p.load(resourceAsStream);
200                String jdbcUrl =  p.getProperty(AdagioConfigurationOption.JDBC_URL.getKey());
201                boolean serverMode = jdbcUrl != null && jdbcUrl.startsWith("jdbc:hsqldb:hsql://");
202                        
203                // If hsqld run on server mode 
204                if (serverMode) {
205                    // Do not copy DB files, but display a warn
206                    log.warn(String.format("Database running in server mode ! Please remove the property '%s' in file %s, to use a file database.", AdagioConfigurationOption.JDBC_URL.getKey(), configFilename));
207                }
208                else {              
209                    // Copy DB files into test directory
210                    copyDb(new File(dbDirectory), "db", !writeDb, null);
211    
212                    // Update db directory with the new path
213                    dbDirectory = new File(resourceDirectory, "db").getAbsolutePath();
214                    dbDirectory = dbDirectory.replaceAll("[\\\\]", "/");
215                }
216            } else {
217                // Load db config properties
218                File dbConfig = new File(dbDirectory, "allegro.properties");
219                Properties p = new Properties();
220                BufferedReader reader = Files.newReader(dbConfig, Charsets.UTF_8);
221                p.load(reader);
222                reader.close();
223
224                if (log.isDebugEnabled()) {
225                    log.debug("Db config: " + dbConfig + "\n" + p);
226                }
227
228                // make sure db is on readonly mode
229                String readonly = p.getProperty("readonly");
230                Preconditions.checkNotNull(readonly, "Could not find readonly property on db confg: " + dbConfig);
231                Preconditions.checkState("true".equals(readonly), "readonly property must be at true value in read mode test in  db confg: "
232                        + dbConfig);
233            }
234        }
235
236        // Initialize configuration
237        initConfiguration(configFilename);  
238        
239        // Init i18n
240        initI18n();
241
242        // Initialize spring context
243        if (beanFactoryReferenceLocation != null) {
244            ServiceLocator.instance().init(
245                    beanFactoryReferenceLocation,
246                    beanRefFactoryReferenceId);
247        }
248    }
249
250    protected final Set<File> toDestroy = Sets.newHashSet();
251
252    public void addToDestroy(File dir) {
253        toDestroy.add(dir);
254    }
255
256    public void setProperty(File file, String key, String value) throws IOException {
257        // Load old properties values
258        Properties props = new Properties();
259        BufferedReader reader = Files.newReader(file, Charsets.UTF_8);
260        props.load(reader);
261        reader.close();
262
263        // Store new properties values
264        props.setProperty(key, value);
265        BufferedWriter writer = Files.newWriter(file, Charsets.UTF_8);
266        props.store(writer, "");
267        writer.close();
268    }
269
270    public void copyDb(File sourceDirectory, String targetDbDirectoryName, boolean readonly, Properties p) throws IOException {
271        File targetDirectory = getResourceDirectory(targetDbDirectoryName);
272        copyDb(sourceDirectory, targetDirectory, readonly, p, true);
273    }
274    
275    public void copyDb(File sourceDirectory, File targetDirectory, boolean readonly, Properties p, boolean destroyAfterTest) throws IOException {
276        if (!sourceDirectory.exists()) {
277
278            if (log.isWarnEnabled()) {
279                log.warn("Could not find db at " + sourceDirectory + ", test [" +
280                            testClass + "] is skipped.");
281            }
282            Assume.assumeTrue(false);
283        }
284        
285        if (p != null) {
286            String jdbcUrl = DaoUtils.getJdbcUrl(targetDirectory, "allegro");
287            DaoUtils.fillConnectionProperties(p, jdbcUrl, "SA", "");
288        }
289        
290        // Add to destroy files list
291        if (destroyAfterTest) {
292            addToDestroy(targetDirectory);
293        }
294
295        log.debug(String.format("Copy directory %s at %s", sourceDirectory.getPath(), targetDirectory.getPath()));
296        FileUtils.copyDirectory(sourceDirectory, targetDirectory);
297
298        // Set readonly property
299        log.debug(String.format("Set database properties with readonly=%s", readonly));
300        File dbConfig = new File(targetDirectory, "allegro.properties");
301        setProperty(dbConfig, "readonly", String.valueOf(readonly));
302    }
303
304    protected void after(Description description) throws IOException {
305        if (log.isInfoEnabled()) {
306            log.info("After test " + testClass);
307        }
308        
309        ServiceLocator serviceLocator = ServiceLocator.instance();
310
311        if (serviceLocator.isOpen()) {
312            // Shutdown HSQLDB database
313            Properties connectionProperties = AdagioConfiguration.getInstance().getConnectionProperties(); 
314            try {
315                DaoUtils.shutdownDatabase(connectionProperties);
316            } catch (Exception e) {
317                if (log.isErrorEnabled()) {
318                    log.error("Could not close database.", e);
319                }
320                witherror = true;
321            }
322    
323            // Shutdown spring context
324            serviceLocator.shutdown();
325        }
326
327        if (!witherror) {
328            for (File file : toDestroy) {
329                if (file.exists()) {
330                    if (log.isInfoEnabled()) {
331                        log.info("Destroy directory: " + file);
332                    }
333                    try {
334                        FileUtils.deleteDirectory(file);
335                    } catch (IOException e) {
336                        if (log.isErrorEnabled()) {
337                            log.error("Could not delete directory: " + file, e);
338                        }
339                    }
340                }
341            }
342        }
343
344        if (beanFactoryReferenceLocation != null) {
345
346            // push back default configuration
347            ServiceLocator.instance().init(null, null);
348        }
349    }
350
351    public Connection createEmptyDb(String dbDirectory,
352                                    String dbName) throws IOException, SQLException {
353        return createEmptyDb(dbDirectory, dbName, null);
354    }
355
356    public Connection createEmptyDb(String dbDirectory,
357                                    String dbName, Properties p) throws IOException, SQLException {
358        File externalDbFile = getResourceDirectory(dbDirectory);
359        return createEmptyDb(externalDbFile, dbName, p);
360    }
361
362    public static File getTestSpecificDirectory(Class<?> testClass,
363                                                String name) throws IOException {
364        // Trying to look for the temporary folder to store data for the test
365        String tempDirPath = System.getProperty("java.io.tmpdir");
366        if (tempDirPath == null) {
367            // can this really occur ?
368            tempDirPath = "";
369            if (log.isWarnEnabled()) {
370                log.warn("'\"java.io.tmpdir\" not defined");
371            }
372        }
373        File tempDirFile = new File(tempDirPath);
374
375        // create the directory to store database data
376        String dataBasePath = testClass.getName()
377                                + File.separator // a directory with the test class name
378                + name // a sub-directory with the method name
379                + '_'
380                                + BUILD_TIMESTAMP; // and a timestamp
381        File databaseFile = new File(tempDirFile, dataBasePath);
382        FileUtils.forceMkdir(databaseFile);
383
384        return databaseFile;
385    }
386
387    public Connection createEmptyDb(File directory,
388                                    String dbName) throws SQLException, IOException {
389
390        return createEmptyDb(directory, dbName, null);
391    }
392
393    public Connection createEmptyDb(File directory,
394                                    String dbName, Properties p) throws SQLException, IOException {
395
396        if (log.isInfoEnabled()) {
397            log.info("Create new db at " + directory);
398        }
399        addToDestroy(directory);
400        String jdbcUrl = DaoUtils.getJdbcUrl(directory, dbName);
401        String user = "SA";
402        String password = "";
403
404        if (p != null) {
405            DaoUtils.fillConnectionProperties(p, jdbcUrl, user, password);
406        }
407        File scriptFile = new File(HSQLDB_SRC_DATABASE_CREATE_SCRIPT);
408        Preconditions.checkState(scriptFile.exists(), "Could not find db script at " + scriptFile);
409
410        if (log.isInfoEnabled()) {
411            log.info("Will use create script: " + scriptFile);
412        }
413        Connection connection = DaoUtils.createConnection(jdbcUrl, user, password);
414
415        if (log.isInfoEnabled()) {
416            log.info("Created connection at " + connection.getMetaData().getURL());
417        }
418
419        List<String> importScriptSql = getImportScriptSql(scriptFile);
420        for (String sql : importScriptSql) {
421            try {               
422                PreparedStatement statement = connection.prepareStatement(sql);
423                statement.execute();
424            }
425            catch(SQLException sqle) {
426                log.warn("SQL command failed : " + sql, sqle);
427                connection.close();
428                throw sqle;
429            }
430        }
431        connection.commit();
432        return connection;
433    }
434
435    protected List<String> getImportScriptSql(File scriptFile) throws IOException {
436        List<String> lines = Files.readLines(scriptFile, Charsets.UTF_8);
437
438        List<String> result = Lists.newArrayListWithCapacity(lines.size());
439
440        Predicate<String> predicate = new Predicate<String>() {
441
442            Set<String> forbiddenStarts = Sets.newHashSet(
443                    "SET ",
444                    "CREATE USER ",
445                    "CREATE SCHEMA ",
446                    "GRANT DBA TO ");
447
448            @Override
449            public boolean apply(String input) {
450                boolean accept = true;
451                for (String forbiddenStart : forbiddenStarts) {
452                    if (input.startsWith(forbiddenStart)) {
453                        accept = false;
454                        break;
455                    }
456                }
457                return accept;
458            }
459        };
460        for (String line : lines) {
461            if (predicate.apply(line.trim().toUpperCase())) {
462                if (line.contains("\\u000a")) {
463                    line = line.replaceAll("\\\\u000a", "\n");
464                }
465                result.add(line);
466            }
467        }
468        return result;
469    }
470    
471    public String getBuildEnvironment() {
472        return getBuildEnvironment(null);
473    }
474
475    protected String getBuildEnvironment(String defaultEnvironement) {
476        String buildEnv = System.getProperty("env");
477
478        // Check validity
479        if (buildEnv == null && StringUtils.isNotBlank(defaultEnvironement)) {
480            buildEnv = defaultEnvironement;
481            log.warn("Could not find build environment. Please add -Denv=<hsqldb|oracle|pgsql>. Test [" +
482                    testClass + "] will use default environment : " + defaultEnvironement);
483        } else if ("hsqldb".equals(buildEnv) == false
484                && "oracle".equals(buildEnv) == false
485                && "pgsql".equals(buildEnv) == false) {
486
487            if (log.isWarnEnabled()) {
488                log.warn("Could not find build environment. Please add -Denv=<hsqldb|oracle|pgsql>. Test [" +
489                        testClass + "] will be skipped.");
490            }
491            Assume.assumeTrue(false);
492        }
493        return buildEnv;
494    }   
495    
496    protected String[] getConfigArgs() {
497        List<String> configArgs = Lists.newArrayList();
498        configArgs.addAll(Lists.newArrayList(
499                "--option", "adagio.basedir", resourceDirectory.getAbsolutePath()));
500//      configArgs.addAll(Lists.newArrayList(
501//              "--option", "adagio.data.directory", new File(resourceDirectory, "data").getAbsolutePath()));
502        if (dbDirectory != null) {
503            configArgs.addAll(Lists.newArrayList("--option", "adagio.persistence.db.directory", dbDirectory));
504        }
505        return configArgs.toArray(new String[configArgs.size()]);
506    }
507    
508    /**
509     * Convenience methods that could be override to initialize other configuration
510     * @param configFilename
511     * @param configArgs
512     */
513    protected void initConfiguration(String configFilename) {
514        String[] configArgs = getConfigArgs();
515        AdagioConfiguration config = new AdagioConfiguration(configFilename, configArgs);
516        AdagioConfiguration.setInstance(config);
517    }
518    
519    protected void initI18n() throws IOException {
520        AdagioConfiguration config = AdagioConfiguration.getInstance();
521        
522        // --------------------------------------------------------------------//
523        // init i18n
524        // --------------------------------------------------------------------//
525        File i18nDirectory = new File(config.getDataDirectory(), "i18n");
526        if (i18nDirectory.exists()) {
527            // clean i18n cache
528            FileUtils.cleanDirectory(i18nDirectory);
529        }
530
531        FileUtils.forceMkdir(i18nDirectory);
532
533        if (log.isDebugEnabled()) {
534            log.debug("I18N directory: " + i18nDirectory);
535        }
536
537        Locale i18nLocale = config.getI18nLocale();
538
539        if (log.isInfoEnabled()) {
540            log.info(String.format("Starts i18n with locale [%s] at [%s]",
541                    i18nLocale, i18nDirectory));
542        }
543        I18n.init(new UserI18nInitializer(
544                i18nDirectory, new DefaultI18nInitializer(getI18nBundleName())),
545                i18nLocale);
546    }
547    
548    protected String getI18nBundleName() {
549        return "adagio-core-shared-i18n";
550    }
551}