View Javadoc
1   package fr.ifremer.quadrige3.core.test;
2   
3   /*-
4    * #%L
5    * Quadrige3 Core :: Quadrige3 Core Shared
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   * 
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   * 
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  
27  import com.google.common.base.Charsets;
28  import com.google.common.collect.Lists;
29  import com.google.common.collect.Sets;
30  import com.google.common.io.Files;
31  import fr.ifremer.quadrige3.core.config.QuadrigeConfiguration;
32  import fr.ifremer.quadrige3.core.config.QuadrigeConfigurationOption;
33  import fr.ifremer.quadrige3.core.dao.technical.Assert;
34  import fr.ifremer.quadrige3.core.dao.technical.Daos;
35  import fr.ifremer.quadrige3.core.dao.technical.DatabaseSchemaDao;
36  import fr.ifremer.quadrige3.core.dao.technical.hibernate.DatabaseSchemaDaoImpl;
37  import fr.ifremer.quadrige3.core.service.ServiceLocator;
38  import org.apache.commons.collections4.CollectionUtils;
39  import org.apache.commons.io.FileUtils;
40  import org.apache.commons.lang3.StringUtils;
41  import org.apache.commons.logging.Log;
42  import org.apache.commons.logging.LogFactory;
43  import org.junit.Assume;
44  import org.junit.rules.TestRule;
45  import org.junit.runner.Description;
46  import org.junit.runners.model.Statement;
47  import org.nuiton.i18n.I18n;
48  import org.nuiton.i18n.init.DefaultI18nInitializer;
49  import org.nuiton.i18n.init.UserI18nInitializer;
50  
51  import java.io.*;
52  import java.sql.Connection;
53  import java.sql.SQLException;
54  import java.util.List;
55  import java.util.Locale;
56  import java.util.Properties;
57  import java.util.Set;
58  
59  /**
60   * To be able to manage database connection for unit test.
61   *
62   * @author blavenie <benoit.lavenier@e-is.pro>
63   * @since 3.3.3
64   */
65  public abstract class DatabaseResource implements TestRule {
66  
67      /** Logger. */
68      protected static final Log log = LogFactory.getLog(DatabaseResource.class);
69  
70      /** Constant <code>BUILD_ENVIRONMENT_DEFAULT="hsqldb"</code> */
71      public static final String BUILD_ENVIRONMENT_DEFAULT = "hsqldb";
72      /** Constant <code>HSQLDB_SRC_DATABASE_DIRECTORY= ie : "../quadrige3-core-client/src/test/db"</code> */
73      public static final String HSQLDB_SRC_DATABASE_DIRECTORY_PATTERN = "../%s/src/test/db";
74      public static final String HSQLDB_SRC_DATABASE_NAME = "quadrige3";
75      public static final String HSQLDB_SRC_DATABASE_SCRIPT_FILE = HSQLDB_SRC_DATABASE_NAME + ".script";
76      public static final String HSQLDB_SRC_DATABASE_PROPERTIES_FILE = HSQLDB_SRC_DATABASE_NAME + ".properties";
77  
78      /** Constant <code>BUILD_TIMESTAMP=System.nanoTime()</code> */
79      public static final long BUILD_TIMESTAMP = System.nanoTime();
80  
81      private File resourceDirectory;
82  
83      private String dbDirectory;
84  
85      private final String beanFactoryReferenceLocation;
86  
87      private final String beanRefFactoryReferenceId;
88  
89      private final boolean writeDb;
90  
91      private final String configName;
92  
93      private boolean witherror = false;
94  
95      private Class<?> testClass;
96  
97      /**
98       * <p>Constructor for DatabaseResource.</p>
99       *
100      * @param configName a {@link String} object.
101      * @param beanFactoryReferenceLocation a {@link String} object.
102      * @param beanRefFactoryReferenceId a {@link String} object.
103      * @param writeDb a boolean.
104      */
105     protected DatabaseResource(String configName, String beanFactoryReferenceLocation,
106             String beanRefFactoryReferenceId,
107             boolean writeDb) {
108         this.configName = configName;
109         this.beanFactoryReferenceLocation = beanFactoryReferenceLocation;
110         this.beanRefFactoryReferenceId = beanRefFactoryReferenceId;
111         this.writeDb = writeDb;
112     }
113 
114     /**
115      * Return configuration files prefix (i.e. 'quadrige3-test')
116      * Could be override by external project
117      *
118      * @return the prefix to use to retrieve configuration files
119      */
120     protected abstract String getConfigFilesPrefix();
121 
122     protected abstract String getModuleDirectory();
123 
124     protected String getHsqldbSrcDatabaseDirectory() {
125         return String.format(HSQLDB_SRC_DATABASE_DIRECTORY_PATTERN, getModuleDirectory());
126     }
127 
128     protected String getHsqldbSrcCreateScript() {
129         return String.format("%s/%s", getHsqldbSrcDatabaseDirectory(), HSQLDB_SRC_DATABASE_SCRIPT_FILE);
130     }
131 
132     /**
133      * <p>Getter for the field <code>resourceDirectory</code>.</p>
134      *
135      * @param name a {@link String} object.
136      * @return a {@link File} object.
137      */
138     public File getResourceDirectory(String name) {
139         return new File(resourceDirectory, name);
140     }
141 
142     /**
143      * <p>Getter for the field <code>resourceDirectory</code>.</p>
144      *
145      * @return a {@link File} object.
146      */
147     public File getResourceDirectory() {
148         return resourceDirectory;
149     }
150 
151     public String getBeanRefFactoryReferenceId() {
152         return beanRefFactoryReferenceId;
153     }
154 
155     public String getBeanFactoryReferenceLocation() {
156         return beanFactoryReferenceLocation;
157     }
158 
159     /**
160      * <p>isWriteDb.</p>
161      *
162      * @return a boolean.
163      */
164     protected boolean isWriteDb() {
165         return writeDb;
166     }
167 
168     /** {@inheritDoc} */
169     @Override
170     public Statement apply(final Statement base, final Description description) {
171 
172         return new Statement() {
173             @Override
174             public void evaluate() throws Throwable {
175                 before(description);
176                 try {
177                     base.evaluate();
178                 } catch (Throwable e) {
179                     witherror = true;
180                     log.error(e);
181                 } finally {
182                     after(description);
183                 }
184             }
185         };
186     }
187 
188     /**
189      * <p>before.</p>
190      *
191      * @param description a {@link org.junit.runner.Description} object.
192      * @throws Throwable if any.
193      */
194     protected void before(Description description) throws Throwable {
195         testClass = description.getTestClass();
196 
197         boolean defaultDbName = StringUtils.isEmpty(configName);
198 
199         dbDirectory = null;
200 
201         if (log.isInfoEnabled()) {
202             log.info("Prepare test " + testClass);
203         }
204 
205         resourceDirectory = getTestSpecificDirectory(testClass, "");
206         addToDestroy(resourceDirectory);
207 
208         // Load building env
209         String buildEnvironment = getBuildEnvironment();
210 
211         // check that config file is in classpath (avoid to find out why it does not works...)
212         String configFilename = getConfigFilesPrefix();
213         if (enableDb()) {
214             configFilename += "-" + (writeDb ? "write" : "read");
215         }
216         if (!defaultDbName) {
217             configFilename += "-" + configName;
218         }
219         String configFilenameNoEnv = configFilename + ".properties";
220         if (StringUtils.isNotBlank(buildEnvironment)) {
221             configFilename += "-" + buildEnvironment;
222         }
223         configFilename += ".properties";
224 
225         InputStream resourceAsStream = getClass().getResourceAsStream("/" + configFilename);
226         if (resourceAsStream == null && StringUtils.isNotBlank(buildEnvironment)) {
227             resourceAsStream = getClass().getResourceAsStream("/" + configFilenameNoEnv);
228             Assert.notNull(resourceAsStream, "Could not find " + configFilename + " or " + configFilenameNoEnv + " in test class-path");
229             configFilename = configFilenameNoEnv;
230         }
231         else {
232             Assert.notNull(resourceAsStream, "Could not find " + configFilename + " in test class-path");
233         }
234 
235         // Prepare DB
236         if (enableDb() && BUILD_ENVIRONMENT_DEFAULT.equalsIgnoreCase(buildEnvironment)) {
237 
238             dbDirectory = getHsqldbSrcDatabaseDirectory();
239             if (!defaultDbName) {
240                 dbDirectory += configName;
241             }
242             Tests.checkDbExists(testClass, dbDirectory);
243 
244             if (writeDb) {
245                 Properties p = new Properties();
246                 p.load(resourceAsStream);
247                 String jdbcUrl = p.getProperty(QuadrigeConfigurationOption.JDBC_URL.getKey());
248                 boolean serverMode = jdbcUrl != null && jdbcUrl.startsWith("jdbc:hsqldb:hsql://");
249 
250                 // If hsqld run on server mode
251                 if (serverMode) {
252                     // Do not copy DB files, but display a warn
253                     log.warn(String.format("Database running in server mode ! Please remove the property '%s' in file %s, to use a file database.",
254                             QuadrigeConfigurationOption.JDBC_URL.getKey(), configFilename));
255                 }
256                 else {
257                     // Copy DB files into test directory
258                     copyDb(new File(dbDirectory), "db", false, null);
259 
260                     // Update db directory with the new path
261                     dbDirectory = new File(resourceDirectory, "db").getAbsolutePath();
262                     dbDirectory = dbDirectory.replaceAll("[\\\\]", "/");
263                 }
264             } else {
265                 // Load db config properties
266                 File dbConfig = new File(dbDirectory, getTestDbName() + ".properties");
267                 Properties p = new Properties();
268                 BufferedReader reader = Files.newReader(dbConfig, Charsets.UTF_8);
269                 p.load(reader);
270                 reader.close();
271 
272                 if (log.isDebugEnabled()) {
273                     log.debug("Db config: " + dbConfig + "\n" + p);
274                 }
275 
276                 // make sure db is on readonly mode
277                 String readonly = p.getProperty("readonly");
278                 Assert.notNull(readonly, "Could not find readonly property on db confg: " + dbConfig);
279                 Assert.state("true".equals(readonly), "readonly property must be at true value in read mode test in  db confg: "
280                         + dbConfig);
281             }
282         }
283 
284         // Initialize configuration
285         initConfiguration(configFilename);
286 
287         // Init i18n
288         initI18n();
289 
290         // Initialize spring context
291         if (beanFactoryReferenceLocation != null) {
292             ServiceLocator.instance().init(
293                     beanFactoryReferenceLocation,
294                     beanRefFactoryReferenceId);
295         }
296     }
297 
298     protected final Set<File> toDestroy = Sets.newHashSet();
299 
300     /**
301      * <p>addToDestroy.</p>
302      *
303      * @param dir a {@link File} object.
304      */
305     public void addToDestroy(File dir) {
306         toDestroy.add(dir);
307     }
308 
309     /**
310      * <p>setProperty.</p>
311      *
312      * @param file a {@link File} object.
313      * @param key a {@link String} object.
314      * @param value a {@link String} object.
315      * @throws IOException if any.
316      */
317     public void setProperty(File file, String key, String value) throws IOException {
318         // Load old properties values
319         Properties props = new Properties();
320         BufferedReader reader = Files.newReader(file, Charsets.UTF_8);
321         props.load(reader);
322         reader.close();
323 
324         // Store new properties values
325         props.setProperty(key, value);
326         BufferedWriter writer = Files.newWriter(file, Charsets.UTF_8);
327         props.store(writer, "");
328         writer.close();
329     }
330 
331     /**
332      * <p>copyDb.</p>
333      *
334      * @param sourceDirectory a {@link File} object.
335      * @param targetDbDirectoryName a {@link String} object.
336      * @param readonly a boolean.
337      * @param p a {@link Properties} object.
338      * @throws IOException if any.
339      */
340     public void copyDb(File sourceDirectory, String targetDbDirectoryName, boolean readonly, Properties p) throws IOException {
341         File targetDirectory = getResourceDirectory(targetDbDirectoryName);
342         copyDb(sourceDirectory, targetDirectory, readonly, p, true);
343     }
344 
345     /**
346      * <p>copyDb.</p>
347      *
348      * @param sourceDirectory a {@link File} object.
349      * @param targetDirectory a {@link File} object.
350      * @param readonly a boolean.
351      * @param p a {@link Properties} object.
352      * @param destroyAfterTest a boolean.
353      * @throws IOException if any.
354      */
355     public void copyDb(File sourceDirectory, File targetDirectory, boolean readonly, Properties p, boolean destroyAfterTest) throws IOException {
356         if (!sourceDirectory.exists()) {
357 
358             if (log.isWarnEnabled()) {
359                 log.warn("Could not find db at " + sourceDirectory + ", test [" +
360                         testClass + "] is skipped.");
361             }
362             Assume.assumeTrue(false);
363         }
364 
365         if (p != null) {
366             String jdbcUrl = Daos.getJdbcUrl(targetDirectory, getTestDbName());
367             Daos.fillConnectionProperties(p, jdbcUrl, "SA", "");
368         }
369 
370         // Add to destroy files list
371         if (destroyAfterTest) {
372             addToDestroy(targetDirectory);
373         }
374 
375         log.debug(String.format("Copy directory %s at %s", sourceDirectory.getPath(), targetDirectory.getPath()));
376         FileUtils.copyDirectory(sourceDirectory, targetDirectory);
377 
378         // Set readonly property
379         log.debug(String.format("Set database properties with readonly=%s", readonly));
380         File dbConfig = new File(targetDirectory, getTestDbName() + ".properties");
381         setProperty(dbConfig, "readonly", String.valueOf(readonly));
382     }
383 
384     /**
385      * <p>after.</p>
386      *
387      * @param description a {@link org.junit.runner.Description} object.
388      */
389     protected void after(Description description) {
390         if (log.isInfoEnabled()) {
391             log.info("After test " + testClass);
392         }
393 
394         ServiceLocator serviceLocator = ServiceLocator.instance();
395 
396         // If service and database has been started
397         if (enableDb() && serviceLocator.isOpen()) {
398             Properties connectionProperties = QuadrigeConfiguration.getInstance().getConnectionProperties();
399 
400             // Shutdown if HSQLDB database is a file database (not server mode)
401             if (Daos.isHsqlFileDatabase(Daos.getUrl(connectionProperties))) {
402                 try {
403                     Daos.shutdownDatabase(connectionProperties);
404                 } catch (Exception e) {
405                     if (log.isErrorEnabled()) {
406                         log.error("Could not close database.", e);
407                     }
408                     witherror = true;
409                 }
410             }
411 
412             // Shutdown spring context
413             serviceLocator.shutdown();
414         }
415 
416         if (!witherror) {
417             destroyDirectories(toDestroy, true);
418         }
419 
420         if (beanFactoryReferenceLocation != null) {
421 
422             // push back default configuration
423             ServiceLocator.instance().init(null, null);
424         }
425     }
426 
427     /**
428      * <p>getTestSpecificDirectory.</p>
429      *
430      * @param testClass a {@link Class} object.
431      * @param name a {@link String} object.
432      * @return a {@link File} object.
433      * @throws IOException if any.
434      */
435     public static File getTestSpecificDirectory(Class<?> testClass,
436                                                 String name) throws IOException {
437         // Trying to look for the temporary folder to store data for the test
438         String tempDirPath = System.getProperty("java.io.tmpdir");
439         if (tempDirPath == null) {
440             // can this really occur ?
441             tempDirPath = "";
442             if (log.isWarnEnabled()) {
443                 log.warn("'\"java.io.tmpdir\" not defined");
444             }
445         }
446         File tempDirFile = new File(tempDirPath);
447 
448         // create the directory to store database data
449         String dataBasePath = testClass.getName()
450                 + File.separator // a directory with the test class name
451                 + name // a sub-directory with the method name
452                 + '_'
453                 + BUILD_TIMESTAMP; // and a timestamp
454         File databaseFile = new File(tempDirFile, dataBasePath);
455         FileUtils.forceMkdir(databaseFile);
456 
457         return databaseFile;
458     }
459 
460     /**
461      * <p>createEmptyDb.</p>
462      *
463      * @param dbDirectory a {@link String} object.
464      * @param dbName a {@link String} object.
465      * @return a {@link Connection} object.
466      * @throws SQLException if any.
467      */
468     public Connection createEmptyDb(String dbDirectory,
469             String dbName) throws SQLException {
470         File externalDbFile = getResourceDirectory(dbDirectory);
471         File scriptFile = new File(getHsqldbSrcCreateScript());
472         return createEmptyDb(externalDbFile, dbName, null, scriptFile);
473     }
474 
475     /**
476      * <p>createEmptyDb.</p>
477      *
478      * @param dbDirectory a {@link String} object.
479      * @param dbName a {@link String} object.
480      * @param p a {@link Properties} object.
481      * @return a {@link Connection} object.
482      * @throws SQLException if any.
483      */
484     public Connection createEmptyDb(String dbDirectory,
485             String dbName, Properties p) throws SQLException {
486         File externalDbFile = getResourceDirectory(dbDirectory);
487         File scriptFile = new File(getHsqldbSrcCreateScript());
488         return createEmptyDb(externalDbFile, dbName, p, scriptFile);
489     }
490 
491     /**
492      * <p>createEmptyDb.</p>
493      *
494      * @param directory a {@link File} object.
495      * @param dbName a {@link String} object.
496      * @param p a {@link Properties} object.
497      * @param scriptFile a {@link File} object.
498      * @return a {@link Connection} object.
499      * @throws SQLException if any.
500      */
501     protected Connection createEmptyDb(
502             File directory,
503             String dbName,
504             Properties p,
505             File scriptFile) throws SQLException {
506 
507         QuadrigeConfiguration config = QuadrigeConfiguration.getInstance();
508 
509         if (log.isInfoEnabled()) {
510             log.info("Create new db at " + directory);
511         }
512         addToDestroy(directory);
513         String jdbcUrl = Daos.getJdbcUrl(directory, dbName);
514         String user = "SA";
515         String password = "";
516 
517         p = (p == null) ? config.getConnectionProperties() : p;
518         Daos.fillConnectionProperties(p, jdbcUrl, user, password);
519 
520         Assert.state(scriptFile.exists(), "Could not find db script at " + scriptFile);
521 
522         DatabaseSchemaDao schemaDao = new DatabaseSchemaDaoImpl(config);
523         schemaDao.generateNewDb(directory, true, scriptFile, p, true/*isTemporaryDb*/);
524         Connection connection = Daos.createConnection(jdbcUrl, user, password);
525 
526         if (log.isInfoEnabled()) {
527             log.info("Created connection at " + connection.getMetaData().getURL());
528         }
529         return connection;
530     }
531 
532     /**
533      * <p>getBuildEnvironment.</p>
534      *
535      * @return a {@link String} object.
536      */
537     public String getBuildEnvironment() {
538         return getBuildEnvironment(null);
539     }
540 
541     /**
542      * -- protected methods--
543      *
544      * @param defaultEnvironment a {@link String} object.
545      * @return a {@link String} object.
546      */
547     protected String getBuildEnvironment(String defaultEnvironment) {
548         String buildEnv = System.getProperty("env");
549 
550         // Check validity
551         if (buildEnv == null && StringUtils.isNotBlank(defaultEnvironment)) {
552             buildEnv = defaultEnvironment;
553             log.warn("Could not find build environment. Please add -Denv=<hsqldb|oracle|pgsql>. Test [" +
554                     testClass + "] will use default environment : " + defaultEnvironment);
555         } else if (!"hsqldb".equals(buildEnv)
556                 && !"oracle".equals(buildEnv)
557                 && !"pgsql".equals(buildEnv)) {
558 
559             if (log.isWarnEnabled()) {
560                 log.warn("Could not find build environment. Please add -Denv=<hsqldb|oracle|pgsql>. Test [" +
561                         testClass + "] will be skipped.");
562             }
563             Assume.assumeTrue(false);
564         }
565         return buildEnv;
566     }
567 
568     /**
569      * <p>getConfigArgs.</p>
570      *
571      * @return an array of {@link String} objects.
572      */
573     protected String[] getConfigArgs() {
574         List<String> configArgs = Lists.newArrayList();
575         configArgs.addAll(Lists.newArrayList(
576                 "--option", QuadrigeConfigurationOption.BASEDIR.getKey(), resourceDirectory.getAbsolutePath()));
577         if (dbDirectory != null) {
578             configArgs.addAll(Lists.newArrayList("--option", QuadrigeConfigurationOption.DB_DIRECTORY.getKey(), dbDirectory));
579         }
580         return configArgs.toArray(new String[configArgs.size()]);
581     }
582 
583     /**
584      * Convenience methods that could be override to initialize other configuration
585      *
586      * @param configFilename a {@link String} object.
587      */
588     protected void initConfiguration(String configFilename) {
589         String[] configArgs = getConfigArgs();
590         QuadrigeConfiguration config = new QuadrigeConfiguration(configFilename, configArgs);
591         QuadrigeConfiguration.setInstance(config);
592     }
593 
594     /**
595      * <p>initI18n.</p>
596      *
597      * @throws IOException if any.
598      */
599     protected void initI18n() throws IOException {
600         QuadrigeConfiguration config = QuadrigeConfiguration.getInstance();
601 
602         // --------------------------------------------------------------------//
603         // init i18n
604         // --------------------------------------------------------------------//
605         File i18nDirectory = new File(config.getDataDirectory(), "i18n");
606         if (i18nDirectory.exists()) {
607             // clean i18n cache
608             FileUtils.cleanDirectory(i18nDirectory);
609         }
610 
611         FileUtils.forceMkdir(i18nDirectory);
612 
613         if (log.isDebugEnabled()) {
614             log.debug("I18N directory: " + i18nDirectory);
615         }
616 
617         Locale i18nLocale = config.getI18nLocale();
618 
619         if (log.isDebugEnabled()) {
620             log.debug(String.format("Starts i18n with locale [%s] at [%s]",
621                     i18nLocale, i18nDirectory));
622         }
623         I18n.init(new UserI18nInitializer(
624                 i18nDirectory, new DefaultI18nInitializer(getI18nBundleName())),
625                 i18nLocale);
626     }
627 
628     /**
629      * <p>getI18nBundleName.</p>
630      *
631      * @return a {@link String} object.
632      */
633     protected String getI18nBundleName() {
634         return "quadrige3-core-shared-i18n";
635     }
636 
637     /**
638      * <p>getTestDbName.</p>
639      *
640      * @return a {@link String} object.
641      */
642     protected String getTestDbName() {
643         return HSQLDB_SRC_DATABASE_NAME;
644     }
645 
646     /* -- Internal methods -- */
647 
648     private boolean enableDb() {
649         return beanFactoryReferenceLocation == null || !beanFactoryReferenceLocation.contains("WithNoDb");
650     }
651 
652     private void destroyDirectories(Set<File> toDestroy, boolean retry) {
653         if (CollectionUtils.isEmpty(toDestroy)) {
654             return;
655         }
656 
657         Set<File> directoriesToRetry = Sets.newHashSet(toDestroy);
658         for (File file : toDestroy) {
659             if (file.exists()) {
660                 if (log.isInfoEnabled()) {
661                     log.info("Destroy directory: " + file);
662                 }
663                 try {
664                     FileUtils.deleteDirectory(file);
665 
666                 } catch (IOException e) {
667                     if (retry) {
668                         if (log.isErrorEnabled()) {
669                             log.error("Could not delete directory: " + file + ". Will retry later.");
670                         }
671                         directoriesToRetry.add(file);
672                     }
673                     else {
674                         if (log.isErrorEnabled()) {
675                             log.error("Could not delete directory: " + file + ". Please delete it manually.");
676                         }
677                     }
678                 }
679             }
680         }
681 
682         if (retry && CollectionUtils.isEmpty(directoriesToRetry)) {
683             destroyDirectories(directoriesToRetry, false);
684         }
685     }
686 }