001/* 002 * #%L 003 * Netarchivesuite - harvester 004 * %% 005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library, 006 * the National Library of France and the Austrian National Library. 007 * %% 008 * This program is free software: you can redistribute it and/or modify 009 * it under the terms of the GNU Lesser General Public License as 010 * published by the Free Software Foundation, either version 2.1 of the 011 * License, or (at your option) any later version. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Lesser Public License for more details. 017 * 018 * You should have received a copy of the GNU General Lesser Public 019 * License along with this program. If not, see 020 * <http://www.gnu.org/licenses/lgpl-2.1.html>. 021 * #L% 022 */ 023 024package dk.netarkivet.harvester.datamodel; 025 026import java.beans.PropertyVetoException; 027import java.io.IOException; 028import java.io.InputStream; 029import java.sql.Connection; 030import java.sql.SQLException; 031import java.util.List; 032import java.util.Map; 033 034import org.apache.commons.io.IOUtils; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import com.antiaction.raptor.sql.ExecuteSqlFile; 039import com.mchange.v2.c3p0.ComboPooledDataSource; 040 041import dk.netarkivet.common.CommonSettings; 042import dk.netarkivet.common.exceptions.ArgumentNotValid; 043import dk.netarkivet.common.exceptions.IOFailure; 044import dk.netarkivet.common.utils.DBUtils; 045import dk.netarkivet.common.utils.ExceptionUtils; 046import dk.netarkivet.common.utils.Settings; 047 048/** 049 * This class handles connections to the harvest definition database, and also defines basic logic for checking versions 050 * of tables. 051 * <p> 052 * The statements to create the tables are located in: 053 * <ul> 054 * <li><em>Derby:</em> scripts/sql/createfullhddb.sql</li> 055 * <li><em>MySQL:</em> scripts/sql/createfullhddb.mysql</li> 056 * <li><em>PostgreSQL:</em> scripts/postgresql/netarchivesuite_init.sql</li> 057 * </ul> 058 * <p> 059 * The implementation relies on a connection pool. Once acquired through the get() method, a connection must be 060 * explicitly returned to the pool by calling the release(Connection) method. 061 * <p> 062 * THis class is intended to be used statically, and hence cannot be instantiated and is final. 063 */ 064public final class HarvestDBConnection { 065 066 private static final Logger log = LoggerFactory.getLogger(HarvestDBConnection.class); 067 068 /** The c3p0 pooled datasource backing this implementation. */ 069 private static ComboPooledDataSource dataSource = null; 070 071 /** 072 * Makes sure that the class can't be instantiated, as it is designed to be used statically. 073 */ 074 private HarvestDBConnection() { 075 076 } 077 078 /** 079 * Get a connection to the harvest definition database from the pool. The pool is configured via the following 080 * configuration properties: 081 * <ul> 082 * <li>@see {@link CommonSettings#DB_POOL_MIN_SIZE}</li> 083 * <li>@see {@link CommonSettings#DB_POOL_MAX_SIZE}</li> 084 * <li>@see {@link CommonSettings#DB_POOL_ACQ_INC}</li> 085 * </ul> 086 * Note that the connection obtained must be returned to the pool by calling {@link #release(Connection)}. 087 * 088 * @return a connection to the harvest definition database 089 * @throws IOFailure if we cannot connect to the database (or find the driver). 090 */ 091 public static synchronized Connection get() { 092 DBSpecifics dbSpec = DBSpecifics.getInstance(); 093 String jdbcUrl = getDBUrl(); 094 095 try { 096 if (dataSource == null) { 097 initDataSource(dbSpec, jdbcUrl); 098 // this is only done for embedded database! 099 // For external databases, use the HarvestdatabaseUpdateApplication tool 100 if (dbSpec instanceof DerbyEmbeddedSpecifics) { 101 dbSpec.updateTables(); 102 } 103 } 104 105 return dataSource.getConnection(); 106 } catch (SQLException e) { 107 final String message = "Can't connect to database with DBurl: '" + jdbcUrl + "' using driver '" 108 + dbSpec.getDriverClassName() + "'" + "\n" + ExceptionUtils.getSQLExceptionCause(e); 109 log.warn(message, e); 110 throw new IOFailure(message, e); 111 } 112 113 } 114 115 /** 116 * Update a table by executing all the statements in the updates String array. If newVersion=1 then the table is 117 * created. Note that this method does not make any checks that the SQL statements in the updates parameter actually 118 * update or create the correct table. 119 * 120 * @param table The table to update 121 * @param newVersion The version that the table should end up at 122 * @param updates The SQL update statements that makes the necessary updates. 123 * @throws IOFailure in case of problems in interacting with the database 124 */ 125 protected static void updateTable(final String table, final int newVersion, final String... updates) { 126 Connection c = get(); 127 updateTable(c, table, newVersion, updates); 128 } 129 130 public static void updateTable(Connection c, final String table, final int newVersion, final String... updates) { 131 log.info("Updating table '{}' to version {}", table, newVersion); 132 133 String[] sqlStatements = new String[updates.length + 1]; 134 String updateSchemaversionSql = null; 135 if (newVersion == 1) { 136 updateSchemaversionSql = "INSERT INTO schemaversions(tablename, version) VALUES ('" + table + "', 1)"; 137 } else { 138 updateSchemaversionSql = "UPDATE schemaversions SET version = " + newVersion + " WHERE tablename = '" 139 + table + "'"; 140 } 141 System.arraycopy(updates, 0, sqlStatements, 0, updates.length); 142 sqlStatements[updates.length] = updateSchemaversionSql; 143 144 try { 145 DBUtils.executeSQL(c, sqlStatements); 146 } finally { 147 release(c); 148 } 149 } 150 151 protected static void updateTableVersion(final String table, final int newVersion, final String... updates) { 152 Connection c = get(); 153 updateTableVersion(c, table, newVersion); 154 } 155 156 public static void updateTableVersion(Connection c, final String table, final int newVersion) { 157 log.info("Updating table '{}' to version {}", table, newVersion); 158 String updateSchemaversionSql = null; 159 if (newVersion == 1) { 160 updateSchemaversionSql = "INSERT INTO schemaversions(tablename, version) VALUES ('" + table + "', 1)"; 161 } else { 162 updateSchemaversionSql = "UPDATE schemaversions SET version = " + newVersion + " WHERE tablename = '" 163 + table + "'"; 164 } 165 try { 166 DBUtils.executeSQL(c, updateSchemaversionSql); 167 } finally { 168 release(c); 169 } 170 } 171 172 /** 173 * Method for retrieving the url for the harvest definition database. This url will be constructed from the 174 * base-url, the machine, the port and the directory. If the database is internal, then only the base-url should 175 * have a value. 176 * 177 * @return The url for the harvest definition database. 178 */ 179 public static String getDBUrl() { 180 StringBuilder res = new StringBuilder(); 181 res.append(Settings.get(CommonSettings.DB_BASE_URL)); 182 183 // append the machine part of the url, if it exists. 184 String tmp = Settings.get(CommonSettings.DB_MACHINE); 185 if (!tmp.isEmpty()) { 186 res.append("://"); 187 res.append(tmp); 188 } 189 190 // append the machine part of the url, if it exists. 191 tmp = Settings.get(CommonSettings.DB_PORT); 192 if (!tmp.isEmpty()) { 193 res.append(":"); 194 res.append(tmp); 195 } 196 197 // append the machine part of the url, if it exists. 198 tmp = Settings.get(CommonSettings.DB_DIR); 199 if (!tmp.isEmpty()) { 200 res.append("/"); 201 res.append(tmp); 202 } 203 204 return res.toString(); 205 } 206 207 /** 208 * Closes the underlying data source. 209 */ 210 public static synchronized void cleanup() { 211 if (dataSource == null) { 212 return; 213 } 214 215 try { 216 // Unclosed connections are not supposed to be found. 217 // Anyway log if there are some. 218 int numUnclosedConn = dataSource.getNumBusyConnections(); 219 if (numUnclosedConn > 0) { 220 log.error("There are {} unclosed connections!", numUnclosedConn); 221 } 222 } catch (SQLException e) { 223 log.warn("Could not query pool status", e); 224 } 225 if (dataSource != null) { 226 dataSource.close(); 227 dataSource = null; 228 } 229 } 230 231 /** 232 * Helper method to return a connection to the pool. 233 * 234 * @param connection a connection 235 */ 236 public static synchronized void release(Connection connection) { 237 ArgumentNotValid.checkNotNull(connection, "connection"); 238 try { 239 connection.close(); 240 } catch (SQLException e) { 241 log.error("Failed to close connection", e); 242 } 243 } 244 245 /** 246 * Initializes the connection pool. 247 * 248 * @param dbSpec the object representing the chosen DB target system. 249 * @param jdbcUrl the JDBC URL to connect to. 250 * @throws SQLException 251 */ 252 private static void initDataSource(DBSpecifics dbSpec, String jdbcUrl) throws SQLException { 253 dataSource = new ComboPooledDataSource(); 254 try { 255 dataSource.setDriverClass(dbSpec.getDriverClassName()); 256 } catch (PropertyVetoException e) { 257 final String message = "Failed to set datasource JDBC driver class '" + dbSpec.getDriverClassName() + "'" 258 + "\n"; 259 throw new IOFailure(message, e); 260 } 261 dataSource.setJdbcUrl(jdbcUrl); 262 String username = Settings.get(CommonSettings.DB_USERNAME); 263 if (!username.isEmpty()) { 264 dataSource.setUser(username); 265 } 266 String password = Settings.get(CommonSettings.DB_PASSWORD); 267 if (!password.isEmpty()) { 268 dataSource.setPassword(password); 269 } 270 // Configure pool size 271 dataSource.setMinPoolSize(Settings.getInt(CommonSettings.DB_POOL_MIN_SIZE)); 272 dataSource.setMaxPoolSize(Settings.getInt(CommonSettings.DB_POOL_MAX_SIZE)); 273 dataSource.setAcquireIncrement(Settings.getInt(CommonSettings.DB_POOL_ACQ_INC)); 274 dataSource.setMaxConnectionAge(Settings.getInt(CommonSettings.DB_POOL_MAX_CONNECTION_AGE)); 275 276 // Configure idle connection testing 277 int testPeriod = Settings.getInt(CommonSettings.DB_POOL_IDLE_CONN_TEST_PERIOD); 278 // TODO This looks odd. Why is checkin-testing inside this if statement? 279 if (testPeriod > 0) { 280 dataSource.setIdleConnectionTestPeriod(testPeriod); 281 dataSource 282 .setTestConnectionOnCheckin(Settings.getBoolean(CommonSettings.DB_POOL_IDLE_CONN_TEST_ON_CHECKIN)); 283 String testQuery = Settings.get(CommonSettings.DB_POOL_IDLE_CONN_TEST_QUERY); 284 if (!testQuery.isEmpty()) { 285 dataSource.setPreferredTestQuery(testQuery); 286 } 287 } 288 289 // Configure statement pooling 290 dataSource.setMaxStatements(Settings.getInt(CommonSettings.DB_POOL_MAX_STM)); 291 dataSource.setMaxStatementsPerConnection(Settings.getInt(CommonSettings.DB_POOL_MAX_STM_PER_CONN)); 292 293 // dataSource.setTestConnectionOnCheckout(true); 294 // dataSource.setBreakAfterAcquireFailure(false); 295 // dataSource.setAcquireRetryAttempts(10000); 296 // dataSource.setAcquireRetryDelay(10); 297 298 if (log.isInfoEnabled()) { 299 log.info("Connection pool initialized with the following values:\n" + "- minPoolSize={}\n" 300 + "- maxPoolSize={}\n" + "- acquireIncrement={}\n" + "- maxStatements={}\n" 301 + "- maxStatementsPerConnection={}\n" + "- idleConnTestPeriod={}\n" + "- idleConnTestQuery='{}'\n" 302 + "- idleConnTestOnCheckin={}", dataSource.getMinPoolSize(), dataSource.getMaxPoolSize(), 303 dataSource.getAcquireIncrement(), dataSource.getMaxStatements(), 304 dataSource.getMaxStatementsPerConnection(), dataSource.getIdleConnectionTestPeriod(), 305 dataSource.getPreferredTestQuery(), dataSource.isTestConnectionOnCheckin()); 306 } 307 } 308 309 /** 310 * Execute the sql to update a given table to a given version. 311 * The necessary scripts are bundled into the root of the harvester-core.jar in the directory sql-migration. 312 * The source of these scripts are in one of these directories: 313 * 314 * $BASEDIR/deploy/deploy-core/scripts/postgresql/migration/ 315 * $BASEDIR/deploy/deploy-core/scripts/mysql/migration/ 316 * $BASEDIR/deploy/deploy-core/scripts/derby/migration/ 317 * 318 * To allow the user to update table 'eav_attribute' in postgresql to version 1 319 * the file $BASEDIR/deploy/deploy-core/scripts/postgresql/migration/eav_attribute.1.sql must exist 320 * The postgresql files are during the build-phase put into the sql-migration/postgresql directory 321 * The same holds for mysql and derby. 322 * 323 * @param dbm the type of DBMS (mysql, postgresql,derby) 324 * @param tableName The given table to update 325 * @param version The table version to update to 326 */ 327 public static void executeSql(String dbm, String tableName, int version) { 328 Connection conn = HarvestDBConnection.get(); 329 executeSql(conn, dbm, tableName, version); 330 HarvestDBConnection.release(conn); 331 conn = null; 332 } 333 334 /** 335 * Look for the file sql-migration/${dbm}/${tableName}.${version}.sql inside in the harvester-core.jar file. 336 * 337 * @param conn a valid database connection 338 * @param dbm the name of the DBMS used 339 * @param tableName the name of the table being updated 340 * @param version The new version of the table 341 */ 342 public static void executeSql(Connection conn, String dbm, String tableName, int version) { 343 String resource = "sql-migration/" + dbm + "/" + tableName + "." + version + ".sql"; 344 log.info("Fetching resource {} to update table '{}' to version {} using databasetype {}", resource, tableName, version, dbm); 345 InputStream in = HarvestDBConnection.class.getClassLoader().getResourceAsStream(resource); 346 try { 347 List<Map.Entry<String, String>> statements = ExecuteSqlFile.splitSql(in, "UTF-8", 8192); 348 in.close(); 349 in = null; 350 ExecuteSqlFile.executeStatements(conn, statements); 351 // Update the schemaversions with the new version for this table 352 HarvestDBConnection.updateTable(conn, tableName, version); 353 } catch (IOException e) { 354 throw new IOFailure("Unable to update the table '" + tableName 355 + "' to version '" + version + "' using database '" + dbm + "'", e); 356 } catch (SQLException e) { 357 throw new IOFailure("Unable to update the table '" + tableName 358 + "' to version '" + version + "' using database '" + dbm + "':\n" + ExceptionUtils.getSQLExceptionCause(e), e); 359 } finally { 360 IOUtils.closeQuietly(in); 361 } 362 } 363 364}