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}