/* * Copyright 2015 Patrick Ahlbrecht * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.onyxbits.raccoon.db; import java.io.File; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.HashMap; import java.util.Stack; import org.hsqldb.jdbc.JDBCConnection; import org.hsqldb.persist.HsqlProperties; /** * Takes care of setting up a database connection and coordinates Java/SQL * bridging via DAOs. * * @author patrick * */ public final class DatabaseManager { /** * Filename for the database */ public static final String DBNAME = "raccoondb_4"; private Stack<Connection> pool; private HsqlProperties props; private HashMap<Class<?>, Object> daos; private HashMap<String, Integer> daoversions; /** * Create a new manager. * * @param databaseDir * directory to keep the files in. * @throws SQLException * if the DAO table cannot be initialized. */ public DatabaseManager(File databaseDir) throws SQLException { pool = new Stack<Connection>(); props = new HsqlProperties(); props.setProperty("connection_type", "file:"); props.setProperty("database", new File(databaseDir, DBNAME).getAbsolutePath()); daos = new HashMap<Class<?>, Object>(); daoversions = new HashMap<String, Integer>(); Connection c = new JDBCConnection(props); Statement st = null; ResultSet res = null; try { // Load the DAO version table st = c.createStatement(); st.execute("CREATE TABLE IF NOT EXISTS versions (dao VARCHAR(255), version INT)"); st.close(); st = c.createStatement(); st.execute("SELECT dao, version FROM versions"); res = st.getResultSet(); while (res.next()) { daoversions.put(res.getString(1), res.getInt(2)); } } finally { if (res != null) { res.close(); } if (st != null) { st.close(); } pool.push(c); } } /** * Lookup a DAO by class. Tables are automatically created/updated if * necessary. Trying to access a table from a database file that is newer than * it's dao will result in an {@link IllegalStateException}. * * @param daoclass * class of the database object to get * @return the DAO */ @SuppressWarnings("unchecked") public <T extends DataAccessObject> T get(Class<T> daoclass) { T ret = (T) daos.get(daoclass); if (ret == null) { try { ret = daoclass.newInstance(); } catch (Exception e) { throw new RuntimeException(e); } // Make sure DAO and TABLE versions match! int codeVer = ret.getVersion(); int dbVer = 0; // 0: Table not yet created Integer tmp = daoversions.get(daoclass.getSimpleName()); if (tmp != null) { dbVer = tmp.intValue(); } if (codeVer > dbVer) { Connection c = null; try { c = connect(); c.setAutoCommit(false); ret.upgradeFrom(dbVer, c); versionTo(codeVer, daoclass.getSimpleName(), c); c.commit(); } catch (SQLException e) { try { c.rollback(); } catch (SQLException e1) { throw new RuntimeException(e1); } } finally { try { c.setAutoCommit(true); } catch (SQLException e) { throw new RuntimeException(e); } disconnect(c); } } if (dbVer > codeVer) { throw new IllegalStateException("Database version conflict!"); } ret.setManager(this); daos.put(daoclass, ret); } return ret; } /** * Set the version of a DAO in the version table * * @param version * @param dao * @param c * @throws SQLException */ private static void versionTo(int version, String dao, Connection c) throws SQLException { PreparedStatement st = null; try { st = c.prepareStatement("MERGE INTO versions USING (VALUES (?, ?)) AS vals(x,y) ON versions.dao = vals.x WHEN MATCHED THEN UPDATE SET versions.version=vals.y WHEN NOT MATCHED THEN INSERT VALUES vals.x , vals.y"); st.setString(1, dao); st.setInt(2, version); st.execute(); } finally { if (st != null) { st.close(); } } } /** * Release resources * * @throws SQLException */ public void shutdown() { try { Connection c = connect(); Statement st = c.createStatement(); st.execute("SHUTDOWN"); st.close(); c.close(); while (!pool.isEmpty()) { pool.pop().close(); } } catch (SQLException e) { e.printStackTrace(); } } /** * Called by DAOs to get a connection object * * @return a connection connection from the pool. The connection should be * returned using disconnect() after doing it's unit of work. */ public Connection connect() { if (pool.isEmpty()) { try { // Speedhack: The proper way of creating a connection is by looking it // up via JDBC url. However, that's time consuming and we need the // connection ASAP. return new JDBCConnection(props); } catch (SQLException e) { e.printStackTrace(); return null; } } return pool.pop(); } /** * Called by DAOs to return a connection after they have finished with it. * * @param c * connection to return to the pool (do not close it). */ public void disconnect(Connection c) { try { if (!c.getAutoCommit()) { throw new RuntimeException( "Connection must be set to autocommit before returning it to the pool!"); } else { pool.push(c); } return; } catch (SQLException e) { // The connection is beyond repair. Just abandon it. } try { c.close(); } catch (SQLException e) { // Don't care. } } }