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