View Javadoc
1   package fr.ifremer.quadrige2.core.test;
2   
3   /*-
4    * #%L
5    * Quadrige2 Core :: Quadrige2 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.base.Preconditions;
29  import com.google.common.collect.Lists;
30  import com.google.common.collect.Sets;
31  import com.google.common.io.Files;
32  import fr.ifremer.quadrige2.core.config.Quadrige2Configuration;
33  import fr.ifremer.quadrige2.core.config.Quadrige2ConfigurationOption;
34  import fr.ifremer.quadrige2.core.dao.technical.Daos;
35  import fr.ifremer.quadrige2.core.dao.technical.DatabaseSchemaDao;
36  import fr.ifremer.quadrige2.core.dao.technical.hibernate.DatabaseSchemaDaoImpl;
37  import fr.ifremer.quadrige2.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 : "../quadrige2-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 = "quadrige2";
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 long BUILD_TIMESTAMP = System.nanoTime();
80  
81      private File resourceDirectory;
82  
83      private String dbDirectory;
84  
85      protected final String beanFactoryReferenceLocation;
86  
87      protected final String beanRefFactoryReferenceId;
88  
89      private final boolean writeDb;
90  
91      private String configName;
92  
93      private boolean witherror = false;
94  
95      protected 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. 'quadrige2-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     /**
152      * <p>isWriteDb.</p>
153      *
154      * @return a boolean.
155      */
156     protected boolean isWriteDb() {
157         return writeDb;
158     }
159 
160     /** {@inheritDoc} */
161     @Override
162     public Statement apply(final Statement base, final Description description) {
163 
164         return new Statement() {
165             @Override
166             public void evaluate() throws Throwable {
167                 before(description);
168                 try {
169                     base.evaluate();
170                 } catch (Throwable e) {
171                     witherror = true;
172                     log.error(e);
173                 } finally {
174                     after(description);
175                 }
176             }
177         };
178     }
179 
180     /**
181      * <p>before.</p>
182      *
183      * @param description a {@link org.junit.runner.Description} object.
184      * @throws Throwable if any.
185      */
186     protected void before(Description description) throws Throwable {
187         testClass = description.getTestClass();
188 
189         boolean defaultDbName = StringUtils.isEmpty(configName);
190 
191         dbDirectory = null;
192 //        if (defaultDbName) {
193 //            configName = "db";
194 //        }
195 
196         if (log.isInfoEnabled()) {
197             log.info("Prepare test " + testClass);
198         }
199 
200         resourceDirectory = getTestSpecificDirectory(testClass, "");
201         addToDestroy(resourceDirectory);
202 
203         // Load building env
204         String buildEnvironment = getBuildEnvironment();
205 
206         // check that config file is in classpath (avoid to find out why it does not works...)
207         String configFilename = getConfigFilesPrefix();
208         if (enableDb()) {
209             configFilename += "-" + (writeDb ? "write" : "read");
210         }
211         if (!defaultDbName) {
212             configFilename += "-" + configName;
213         }
214         String configFilenameNoEnv = configFilename + ".properties";
215         if (StringUtils.isNotBlank(buildEnvironment)) {
216             configFilename += "-" + buildEnvironment;
217         }
218         configFilename += ".properties";
219 
220         InputStream resourceAsStream = getClass().getResourceAsStream("/" + configFilename);
221         if (resourceAsStream == null && StringUtils.isNotBlank(buildEnvironment)) {
222             resourceAsStream = getClass().getResourceAsStream("/" + configFilenameNoEnv);
223             Preconditions.checkNotNull(resourceAsStream, "Could not find " + configFilename + " or " + configFilenameNoEnv + " in test class-path");
224             configFilename = configFilenameNoEnv;
225         }
226         else {
227             Preconditions.checkNotNull(resourceAsStream, "Could not find " + configFilename + " in test class-path");
228         }
229 
230         // Prepare DB
231         if (enableDb() && BUILD_ENVIRONMENT_DEFAULT.equalsIgnoreCase(buildEnvironment)) {
232 
233             dbDirectory = getHsqldbSrcDatabaseDirectory();
234             if (!defaultDbName) {
235                 dbDirectory += configName;
236             }
237             Tests.checkDbExists(testClass, dbDirectory);
238 
239             if (writeDb) {
240                 Properties p = new Properties();
241                 p.load(resourceAsStream);
242                 String jdbcUrl = p.getProperty(Quadrige2ConfigurationOption.JDBC_URL.getKey());
243                 boolean serverMode = jdbcUrl != null && jdbcUrl.startsWith("jdbc:hsqldb:hsql://");
244 
245                 // If hsqld run on server mode
246                 if (serverMode) {
247                     // Do not copy DB files, but display a warn
248                     log.warn(String.format("Database running in server mode ! Please remove the property '%s' in file %s, to use a file database.",
249                             Quadrige2ConfigurationOption.JDBC_URL.getKey(), configFilename));
250                 }
251                 else {
252                     // Copy DB files into test directory
253                     copyDb(new File(dbDirectory), "db", !writeDb, null);
254 
255                     // Update db directory with the new path
256                     dbDirectory = new File(resourceDirectory, "db").getAbsolutePath();
257                     dbDirectory = dbDirectory.replaceAll("[\\\\]", "/");
258                 }
259             } else {
260                 // Load db config properties
261                 File dbConfig = new File(dbDirectory, getTestDbName() + ".properties");
262                 Properties p = new Properties();
263                 BufferedReader reader = Files.newReader(dbConfig, Charsets.UTF_8);
264                 p.load(reader);
265                 reader.close();
266 
267                 if (log.isDebugEnabled()) {
268                     log.debug("Db config: " + dbConfig + "\n" + p);
269                 }
270 
271                 // make sure db is on readonly mode
272                 String readonly = p.getProperty("readonly");
273                 Preconditions.checkNotNull(readonly, "Could not find readonly property on db confg: " + dbConfig);
274                 Preconditions.checkState("true".equals(readonly), "readonly property must be at true value in read mode test in  db confg: "
275                         + dbConfig);
276             }
277         }
278 
279         // Initialize configuration
280         initConfiguration(configFilename);
281 
282         // Init i18n
283         initI18n();
284 
285         // Initialize spring context
286         if (beanFactoryReferenceLocation != null) {
287             ServiceLocator.instance().init(
288                     beanFactoryReferenceLocation,
289                     beanRefFactoryReferenceId);
290         }
291     }
292 
293     protected final Set<File> toDestroy = Sets.newHashSet();
294 
295     /**
296      * <p>addToDestroy.</p>
297      *
298      * @param dir a {@link File} object.
299      */
300     public void addToDestroy(File dir) {
301         toDestroy.add(dir);
302     }
303 
304     /**
305      * <p>setProperty.</p>
306      *
307      * @param file a {@link File} object.
308      * @param key a {@link String} object.
309      * @param value a {@link String} object.
310      * @throws IOException if any.
311      */
312     public void setProperty(File file, String key, String value) throws IOException {
313         // Load old properties values
314         Properties props = new Properties();
315         BufferedReader reader = Files.newReader(file, Charsets.UTF_8);
316         props.load(reader);
317         reader.close();
318 
319         // Store new properties values
320         props.setProperty(key, value);
321         BufferedWriter writer = Files.newWriter(file, Charsets.UTF_8);
322         props.store(writer, "");
323         writer.close();
324     }
325 
326     /**
327      * <p>copyDb.</p>
328      *
329      * @param sourceDirectory a {@link File} object.
330      * @param targetDbDirectoryName a {@link String} object.
331      * @param readonly a boolean.
332      * @param p a {@link Properties} object.
333      * @throws IOException if any.
334      */
335     public void copyDb(File sourceDirectory, String targetDbDirectoryName, boolean readonly, Properties p) throws IOException {
336         File targetDirectory = getResourceDirectory(targetDbDirectoryName);
337         copyDb(sourceDirectory, targetDirectory, readonly, p, true);
338     }
339 
340     /**
341      * <p>copyDb.</p>
342      *
343      * @param sourceDirectory a {@link File} object.
344      * @param targetDirectory a {@link File} object.
345      * @param readonly a boolean.
346      * @param p a {@link Properties} object.
347      * @param destroyAfterTest a boolean.
348      * @throws IOException if any.
349      */
350     public void copyDb(File sourceDirectory, File targetDirectory, boolean readonly, Properties p, boolean destroyAfterTest) throws IOException {
351         if (!sourceDirectory.exists()) {
352 
353             if (log.isWarnEnabled()) {
354                 log.warn("Could not find db at " + sourceDirectory + ", test [" +
355                         testClass + "] is skipped.");
356             }
357             Assume.assumeTrue(false);
358         }
359 
360         if (p != null) {
361             String jdbcUrl = Daos.getJdbcUrl(targetDirectory, getTestDbName());
362             Daos.fillConnectionProperties(p, jdbcUrl, "SA", "");
363         }
364 
365         // Add to destroy files list
366         if (destroyAfterTest) {
367             addToDestroy(targetDirectory);
368         }
369 
370         log.debug(String.format("Copy directory %s at %s", sourceDirectory.getPath(), targetDirectory.getPath()));
371         FileUtils.copyDirectory(sourceDirectory, targetDirectory);
372 
373         // Set readonly property
374         log.debug(String.format("Set database properties with readonly=%s", readonly));
375         File dbConfig = new File(targetDirectory, getTestDbName() + ".properties");
376         setProperty(dbConfig, "readonly", String.valueOf(readonly));
377     }
378 
379     /**
380      * <p>after.</p>
381      *
382      * @param description a {@link org.junit.runner.Description} object.
383      * @throws IOException if any.
384      */
385     protected void after(Description description) throws IOException {
386         if (log.isInfoEnabled()) {
387             log.info("After test " + testClass);
388         }
389 
390         ServiceLocator serviceLocator = ServiceLocator.instance();
391 
392         // If service and database has been started
393         if (enableDb() && serviceLocator.isOpen()) {
394             Properties connectionProperties = Quadrige2Configuration.getInstance().getConnectionProperties();
395 
396             // Shutdown if HSQLDB database is a file database (not server mode)
397             if (Daos.isFileDatabase(Daos.getUrl(connectionProperties))) {
398                 try {
399                     Daos.shutdownDatabase(connectionProperties);
400                 } catch (Exception e) {
401                     if (log.isErrorEnabled()) {
402                         log.error("Could not close database.", e);
403                     }
404                     witherror = true;
405                 }
406             }
407 
408             // Shutdown spring context
409             serviceLocator.shutdown();
410         }
411 
412         if (!witherror) {
413             destroyDirectories(toDestroy, true);
414         }
415 
416         if (beanFactoryReferenceLocation != null) {
417 
418             // push back default configuration
419             ServiceLocator.instance().init(null, null);
420         }
421     }
422 
423     /**
424      * <p>getTestSpecificDirectory.</p>
425      *
426      * @param testClass a {@link Class} object.
427      * @param name a {@link String} object.
428      * @return a {@link File} object.
429      * @throws IOException if any.
430      */
431     public static File getTestSpecificDirectory(Class<?> testClass,
432                                                 String name) throws IOException {
433         // Trying to look for the temporary folder to store data for the test
434         String tempDirPath = System.getProperty("java.io.tmpdir");
435         if (tempDirPath == null) {
436             // can this really occur ?
437             tempDirPath = "";
438             if (log.isWarnEnabled()) {
439                 log.warn("'\"java.io.tmpdir\" not defined");
440             }
441         }
442         File tempDirFile = new File(tempDirPath);
443 
444         // create the directory to store database data
445         String dataBasePath = testClass.getName()
446                 + File.separator // a directory with the test class name
447                 + name // a sub-directory with the method name
448                 + '_'
449                 + BUILD_TIMESTAMP; // and a timestamp
450         File databaseFile = new File(tempDirFile, dataBasePath);
451         FileUtils.forceMkdir(databaseFile);
452 
453         return databaseFile;
454     }
455 
456     /**
457      * <p>createEmptyDb.</p>
458      *
459      * @param dbDirectory a {@link String} object.
460      * @param dbName a {@link String} object.
461      * @return a {@link Connection} object.
462      * @throws IOException if any.
463      * @throws SQLException if any.
464      */
465     public Connection createEmptyDb(String dbDirectory,
466             String dbName) throws IOException, SQLException {
467         File externalDbFile = getResourceDirectory(dbDirectory);
468         File scriptFile = new File(getHsqldbSrcCreateScript());
469         return createEmptyDb(externalDbFile, dbName, null, scriptFile);
470     }
471 
472     /**
473      * <p>createEmptyDb.</p>
474      *
475      * @param dbDirectory a {@link String} object.
476      * @param dbName a {@link String} object.
477      * @param p a {@link Properties} object.
478      * @return a {@link Connection} object.
479      * @throws IOException if any.
480      * @throws SQLException if any.
481      */
482     public Connection createEmptyDb(String dbDirectory,
483             String dbName, Properties p) throws IOException, SQLException {
484         File externalDbFile = getResourceDirectory(dbDirectory);
485         File scriptFile = new File(getHsqldbSrcCreateScript());
486         return createEmptyDb(externalDbFile, dbName, p, scriptFile);
487     }
488 
489     /**
490      * <p>createEmptyDb.</p>
491      *
492      * @param directory a {@link File} object.
493      * @param dbName a {@link String} object.
494      * @param p a {@link Properties} object.
495      * @param scriptFile a {@link File} object.
496      * @return a {@link Connection} object.
497      * @throws SQLException if any.
498      * @throws IOException if any.
499      */
500     protected Connection createEmptyDb(
501             File directory,
502             String dbName,
503             Properties p,
504             File scriptFile) throws SQLException, IOException {
505 
506         Quadrige2Configuration config = Quadrige2Configuration.getInstance();
507 
508         if (log.isInfoEnabled()) {
509             log.info("Create new db at " + directory);
510         }
511         addToDestroy(directory);
512         String jdbcUrl = Daos.getJdbcUrl(directory, dbName);
513         String user = "SA";
514         String password = "";
515 
516         p = (p == null) ? config.getConnectionProperties() : p;
517         Daos.fillConnectionProperties(p, jdbcUrl, user, password);
518 
519         Preconditions.checkState(scriptFile.exists(), "Could not find db script at " + scriptFile);
520 
521         DatabaseSchemaDao schemaDao = new DatabaseSchemaDaoImpl(config);
522         schemaDao.generateNewDb(directory, true, scriptFile, p, true/*isTemporaryDb*/);
523         Connection connection = Daos.createConnection(jdbcUrl, user, password);
524 
525         if (log.isInfoEnabled()) {
526             log.info("Created connection at " + connection.getMetaData().getURL());
527         }
528         return connection;
529     }
530 
531     /**
532      * <p>getBuildEnvironment.</p>
533      *
534      * @return a {@link String} object.
535      */
536     public String getBuildEnvironment() {
537         return getBuildEnvironment(null);
538     }
539 
540     /**
541      * -- protected methods--
542      *
543      * @param defaultEnvironment a {@link String} object.
544      * @return a {@link String} object.
545      */
546     protected String getBuildEnvironment(String defaultEnvironment) {
547         String buildEnv = System.getProperty("env");
548 
549         // Check validity
550         if (buildEnv == null && StringUtils.isNotBlank(defaultEnvironment)) {
551             buildEnv = defaultEnvironment;
552             log.warn("Could not find build environment. Please add -Denv=<hsqldb|oracle|pgsql>. Test [" +
553                     testClass + "] will use default environment : " + defaultEnvironment);
554         } else if (!"hsqldb".equals(buildEnv)
555                 && !"oracle".equals(buildEnv)
556                 && !"pgsql".equals(buildEnv)) {
557 
558             if (log.isWarnEnabled()) {
559                 log.warn("Could not find build environment. Please add -Denv=<hsqldb|oracle|pgsql>. Test [" +
560                         testClass + "] will be skipped.");
561             }
562             Assume.assumeTrue(false);
563         }
564         return buildEnv;
565     }
566 
567     /**
568      * <p>getConfigArgs.</p>
569      *
570      * @return an array of {@link String} objects.
571      */
572     protected String[] getConfigArgs() {
573         List<String> configArgs = Lists.newArrayList();
574         configArgs.addAll(Lists.newArrayList(
575                 "--option", Quadrige2ConfigurationOption.BASEDIR.getKey(), resourceDirectory.getAbsolutePath()));
576         if (dbDirectory != null) {
577             configArgs.addAll(Lists.newArrayList("--option", Quadrige2ConfigurationOption.DB_DIRECTORY.getKey(), dbDirectory));
578         }
579         return configArgs.toArray(new String[configArgs.size()]);
580     }
581 
582     /**
583      * Convenience methods that could be override to initialize other configuration
584      *
585      * @param configFilename a {@link String} object.
586      */
587     protected void initConfiguration(String configFilename) {
588         String[] configArgs = getConfigArgs();
589         Quadrige2Configuration config = new Quadrige2Configuration(configFilename, configArgs);
590         Quadrige2Configuration.setInstance(config);
591     }
592 
593     /**
594      * <p>initI18n.</p>
595      *
596      * @throws IOException if any.
597      */
598     protected void initI18n() throws IOException {
599         Quadrige2Configuration config = Quadrige2Configuration.getInstance();
600 
601         // --------------------------------------------------------------------//
602         // init i18n
603         // --------------------------------------------------------------------//
604         File i18nDirectory = new File(config.getDataDirectory(), "i18n");
605         if (i18nDirectory.exists()) {
606             // clean i18n cache
607             FileUtils.cleanDirectory(i18nDirectory);
608         }
609 
610         FileUtils.forceMkdir(i18nDirectory);
611 
612         if (log.isDebugEnabled()) {
613             log.debug("I18N directory: " + i18nDirectory);
614         }
615 
616         Locale i18nLocale = config.getI18nLocale();
617 
618         if (log.isDebugEnabled()) {
619             log.debug(String.format("Starts i18n with locale [%s] at [%s]",
620                     i18nLocale, i18nDirectory));
621         }
622         I18n.init(new UserI18nInitializer(
623                 i18nDirectory, new DefaultI18nInitializer(getI18nBundleName())),
624                 i18nLocale);
625     }
626 
627     /**
628      * <p>getI18nBundleName.</p>
629      *
630      * @return a {@link String} object.
631      */
632     protected String getI18nBundleName() {
633         return "quadrige2-core-shared-i18n";
634     }
635 
636     /**
637      * <p>getTestDbName.</p>
638      *
639      * @return a {@link String} object.
640      */
641     protected String getTestDbName() {
642         return HSQLDB_SRC_DATABASE_NAME;
643     }
644 
645     /* -- Internal methods -- */
646 
647     private boolean enableDb() {
648         return beanFactoryReferenceLocation == null || !beanFactoryReferenceLocation.contains("WithNoDb");
649     }
650 
651     private void destroyDirectories(Set<File> toDestroy, boolean retry) {
652         if (CollectionUtils.isEmpty(toDestroy)) {
653             return;
654         }
655 
656         Set<File> directoriesToRetry = Sets.newHashSet(toDestroy);
657         for (File file : toDestroy) {
658             if (file.exists()) {
659                 if (log.isInfoEnabled()) {
660                     log.info("Destroy directory: " + file);
661                 }
662                 try {
663                     FileUtils.deleteDirectory(file);
664 
665                 } catch (IOException e) {
666                     if (retry) {
667                         if (log.isErrorEnabled()) {
668                             log.error("Could not delete directory: " + file + ". Will retry later.");
669                         }
670                         directoriesToRetry.add(file);
671                     }
672                     else {
673                         if (log.isErrorEnabled()) {
674                             log.error("Could not delete directory: " + file + ". Please delete it manually.");
675                         }
676                     }
677                 }
678             }
679         }
680 
681         if (retry && CollectionUtils.isEmpty(directoriesToRetry)) {
682             destroyDirectories(directoriesToRetry, false);
683         }
684     }
685 }