Java tutorial
/** * DbEngine.java * * Copyright (c) 2013 CyrusBuilt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.cyrusbuilt.guncabinet.dao; import java.lang.IllegalStateException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Savepoint; import java.sql.SQLException; import java.util.Enumeration; import java.util.Vector; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.lang3.StringUtils; import org.cyrusbuilt.guncabinet.IDisposable; import org.cyrusbuilt.guncabinet.ObjectDisposedException; import org.cyrusbuilt.guncabinet.dao.DbDriverNotFoundException; import org.cyrusbuilt.guncabinet.dao.entities.ChamberCaliberEntity; import org.cyrusbuilt.guncabinet.dao.events.TableUpdateEventListener; import org.cyrusbuilt.guncabinet.dao.events.TableUpdateFailedEventArgs; import org.cyrusbuilt.guncabinet.dao.events.TableUpdateFinishedEventArgs; import org.cyrusbuilt.guncabinet.dao.events.TableUpdateStartEventArgs; import org.joda.time.DateTime; import com.mysql.jdbc.Statement; /** * * @author cyrusbuilt */ public final class DbEngine implements IDisposable { private static Boolean _isDisposed = false; private static Integer _refCount = 0; private static DbEngine _instance = null; private static final Lock _padLock = new ReentrantLock(); private String _host = StringUtils.EMPTY; private String _dbName = StringUtils.EMPTY; private String _username = StringUtils.EMPTY; private Boolean _isConnected = false; private Connection _conn = null; private Statement _stmt = null; private char[] _password = {}; private Vector<TableUpdateEventListener> _eventListeners = null; /** * Initializes a new instance of the {@link org.cyrusbuilt.guncabinet.dao.DbEngine} * class. Private constructor to prevent normal instantiation. */ private DbEngine() { } /** * Guarantees a thread-safe singleton instance of this engine using a * double-check locking mechanism. * @return If this is the first time this accessor has been called, then * a new instance of this engine is returned; Otherwise, a previously * created instance is returned. */ public static DbEngine instance() { Boolean locked = false; try { locked = _padLock.tryLock(); _refCount++; if (_instance == null) { _instance = new DbEngine(); _isDisposed = false; } } finally { if (locked) { _padLock.unlock(); } } return _instance; } /** * Releases all resources in use by the {@link com.mat.mealtrack.dao.DbContext} component. * @param disposing Set true if *actually* disposing. Finalizers * should set this parameter to false. */ private void dispose(Boolean disposing) { if (disposing) { Boolean locked = false; try { locked = _padLock.tryLock(); if (_refCount == 0) { _instance = null; _isDisposed = true; this._isConnected = false; try { if (this._stmt != null) { this._stmt.close(); this._stmt = null; } if (this._conn != null) { this._conn.close(); this._conn = null; } } catch (SQLException e) { // Eat it. We should NEVER throw an exception on dispose. } if (this._eventListeners != null) { this._eventListeners.clear(); this._eventListeners = null; } } } finally { if (locked) { _padLock.unlock(); } } } } /* (non-Javadoc) * @see org.cyrusbuilt.guncabinet.IDisposable#dispose() */ @Override public void dispose() { if (_isDisposed) { return; } _refCount--; this.dispose(true); } /** * Called by the garbage collector when this object is found to be discarded. */ @Override protected void finalize() { try { this.dispose(false); super.finalize(); } catch (Throwable e) { // Eat it. We should NEVER throw an exception on finalize. } } /** * Gets whether or not this instance has been disposed. * @return true if disposed; Otherwise, false. */ public Boolean isDisposed() { return _isDisposed; } /** * Gets whether or not {@link org.cyrusbuilt.guncabinet.dao.DbEngine} is * currently connected to the database. * @return true if connected; Otherwise, false. */ public Boolean isConnected() { return this._isConnected; } /** * Gets the database host name or IP. * @return The host name or IP of the MySQL database host. */ public String dbHost() { return this._host; } /** * Sets host name or IP address of the database host. * @param host The host name or IP of the MySQL database host. */ public void setDbHost(String host) { this._host = host; } /** * Gets the name of the database to connect to. * @return The name of the database to connect to, if set; Otherwise, an * empty string. */ public String dbName() { return this._dbName; } /** * Sets the name of the database to connect to. * @param dbName The name of the database to connect to. */ public void setDbName(String dbName) { this._dbName = dbName; } /** * Gets the username used to authenticate against the database. * @return The username to authenticate. */ public String username() { return this._username; } /** * Sets the username to authenticate with the database. * @param username The username to authenticate. */ public void setUsername(String username) { this._username = username; } /** * Sets the password for the user to authenticate. * @param password The password used to authenticate the user. */ public void setPassword(char[] password) { this._password = password; } /** * Adds an event listener. * @param listener The listener for table update events. */ public void addTableUpdateEventListener(TableUpdateEventListener listener) { if (this._eventListeners == null) { this._eventListeners = new Vector<TableUpdateEventListener>(); } this._eventListeners.add(listener); } /** * Removes an event listener. * @param listener The table update listener to remove. */ public void removeTableUpdateEventListener(TableUpdateEventListener listener) { if ((this._eventListeners != null) && (!this._eventListeners.isEmpty())) { this._eventListeners.remove(listener); } } /** * Fires the table update started event. This will invoke all listeners for * this event. * @param e The event arguments. */ private void notifyTableUpdateStartedListeners(TableUpdateStartEventArgs e) { if ((this._eventListeners != null) && (!this._eventListeners.isEmpty())) { Enumeration<TableUpdateEventListener> elmnts = this._eventListeners.elements(); TableUpdateEventListener event = null; while (elmnts.hasMoreElements()) { event = elmnts.nextElement(); event.onTableUpdateStarted(e); } elmnts = null; } } /** * Fires the table update failed event. This will invoke all listeners for * this event. * @param e The event arguments. */ private void notifyTableUpdateFailedListeners(TableUpdateFailedEventArgs e) { if ((this._eventListeners != null) && (!this._eventListeners.isEmpty())) { Enumeration<TableUpdateEventListener> elmnts = this._eventListeners.elements(); TableUpdateEventListener event = null; while (elmnts.hasMoreElements()) { event = elmnts.nextElement(); event.onTableUpdateFailed(e); } elmnts = null; } } /** * Fires the table update finished event. This will invoke all listeners for * this event. * @param e The event arguments. */ private void notifyTableUpdateFinishedListeners(TableUpdateFinishedEventArgs e) { if ((this._eventListeners != null) && (!this._eventListeners.isEmpty())) { Enumeration<TableUpdateEventListener> elmnts = this._eventListeners.elements(); TableUpdateEventListener event = null; while (elmnts.hasMoreElements()) { event = elmnts.nextElement(); event.onTableUpdateFinished(e); } elmnts = null; } } /** * Establishes a connection to the database, then prepares a {@link com.mysql.jdbc.Statement} * object that can be used to perform queries. * @throws SQLException A database connection failure occurred. * @throws ObjectDisposedException This instance has been disposed and * can no longer be used. * @throws DbDriverNotFoundException The MySQL JDBC driver could not be * found in the class path. */ public void connect() throws SQLException, ObjectDisposedException, DbDriverNotFoundException { if (_isDisposed) { throw new ObjectDisposedException(this); } if (this._isConnected) { return; } // Load the JDBC driver. final String driverName = "com.mysql.jdbc.Driver"; try { Class.forName(driverName); } catch (ClassNotFoundException e) { throw new DbDriverNotFoundException(driverName); } // Build connection string. final String connString = "jdbc:mysql://" + this._host + "/" + this._dbName + "?user=" + this._username + "&password=" + (new String(this._password)); this._conn = DriverManager.getConnection(connString); this._stmt = (Statement) this._conn.createStatement(); this._isConnected = true; } /** * Gets the inner database connection object. This is useful for when * direct access to the underlying connection is needed (ie. For building * {@link com.mysql.jdbc.PreparedStatement}s). * @return The inner database connection object ({@link java.sql.Connection}). * @exception IllegalStateException Cannot get inner connection object * because this instance is not yet connected to the database. */ public Connection getInnerDbConnection() { if (!this._isConnected) { throw new IllegalStateException("Not connected to database."); } return this._conn; } /** * Gets the statement object which allows executing SQL queries against the DB. * @return The statement object instance. * @exception IllegalStateException Not yet connected to the database. At least * one call to the {@link com.mat.mealtrack.dao.DbContext#connect()} method must * be made before calling this method. */ public Statement getStatement() { if (!this._isConnected) { throw new IllegalStateException("Not connected to database. Cannot request Statement for query."); } return this._stmt; } /** * Disconnects from the database. If already disconnected, this method does * nothing. Otherwise, all connections are broken. * @throws ObjectDisposedException This instance has been disposed. Therefore, * you can no longer interact with this object. */ public void disconnect() throws ObjectDisposedException { if (_isDisposed) { throw new ObjectDisposedException(this); } if (!this._isConnected) { return; } try { if (this._stmt != null) { this._stmt.close(); } if (this._conn != null) { this._conn.close(); } } catch (SQLException ex) { // Eat it. } finally { this._isConnected = false; } } /** * Marks the start of a table update. This will notify any table update listeners. * @param table The table being updated. * @return The time the update started. */ private DateTime beginUpdate(Tables table) { DateTime dt = DateTime.now(); this.notifyTableUpdateStartedListeners(new TableUpdateStartEventArgs(dt, table)); return dt; } /** * Marks the end of the table update. This will notify any table update listeners. * @param table The table that was updated. * @param start The time the table update started, as returned by {@link org.cyrusbuilt.guncabinet.dao.DbEngine#beginUpdate(Tables)}. * @param recordId The ID of the record affected in the specified table. */ private void endUpdate(Tables table, DateTime start, Integer recordId) { TableUpdateFinishedEventArgs args = new TableUpdateFinishedEventArgs(start, table, recordId); this.notifyTableUpdateFinishedListeners(args); } /** * Gets whether or not the specified caliber exists in the database. * @param id The ID of the caliber to check for. * @return true if the caliber exists; Otherwise, false. * @throws ObjectDisposedException This instance has been disposed. * @throws SQLException A database access error occurred. * @throws DbDriverNotFoundException MySQL database driver was not found. */ public Boolean caliberExists(Integer id) throws ObjectDisposedException, SQLException, DbDriverNotFoundException { if (_isDisposed) { throw new ObjectDisposedException(this); } if (!this._isConnected) { this.connect(); } Boolean exists = false; String query = "select Id from " + this._dbName + "." + DbUtils.getTableName(Tables.ChamberCaliber) + " where Id='" + id.toString() + "'"; ResultSet rs = this._stmt.executeQuery(query); exists = rs.next(); rs.close(); rs = null; return exists; } /** * Gets the specified caliber. * @param id The ID of the caliber to retrieve. * @return If successful, a {@link org.cyrusbuilt.guncabinet.dao.entities.ChamberCaliberEntity} * matching the specified ID; Otherwise, null. * @throws ObjectDisposedException This instance has been disposed. * @throws SQLException A database access error occurred. * @throws DbDriverNotFoundException The MySQL database driver was not found. */ public ChamberCaliberEntity getCaliber(Integer id) throws ObjectDisposedException, SQLException, DbDriverNotFoundException { if (_isDisposed) { throw new ObjectDisposedException(this); } if (id <= 0) { return null; } ChamberCaliberEntity caliber = null; String query = "select * from " + this._dbName + "." + DbUtils.getTableName(Tables.ChamberCaliber) + " where Id='" + id.toString() + "'"; ResultSet rs = this._stmt.executeQuery(query); if (rs.first()) { caliber = new ChamberCaliberEntity(rs.getInt("Id"), rs.getString("Name")); caliber.setNotes(rs.getString("Notes")); caliber.setNATO(rs.getBoolean("IsNATO")); } rs.close(); return caliber; } /** * * @param caliber * @exception IllegalArgumentException * @throws ObjectDisposedException * @throws SQLException * @throws DbDriverNotFoundException */ public void insertOrUpdateCaliber(ChamberCaliberEntity caliber) throws ObjectDisposedException, SQLException, DbDriverNotFoundException { if (caliber == null) { throw new IllegalArgumentException("caliber cannot be null."); } if (_isDisposed) { throw new ObjectDisposedException(this); } if (!this._isConnected) { this.connect(); } DateTime start = this.beginUpdate(Tables.ChamberCaliber); this._conn.setAutoCommit(false); Savepoint sp = this._conn.setSavepoint(); PreparedStatement ps = null; Integer result = 0; Integer id = 0; if (this.caliberExists(caliber.getId())) { // Record already exists, so we'll just update the values. String update = "update " + this._dbName + "." + DbUtils.getTableName(Tables.ChamberCaliber) + " set Name='" + caliber.name() + "', Notes='" + caliber.notes() + "', IsNATO='" + caliber.isNATO().toString() + "' where Id='" + caliber.getId().toString() + "'"; ps = this._conn.prepareStatement(update); result = ps.executeUpdate(); id = caliber.getId(); } else { // This is a new record. String insert = "insert into " + this._dbName + "." + DbUtils.getTableName(Tables.ChamberCaliber) + " values (NUll, ?, ?, ?)"; ps = this._conn.prepareStatement(insert, PreparedStatement.RETURN_GENERATED_KEYS); ps.setString(1, caliber.name()); ps.setString(2, caliber.notes()); ps.setString(3, caliber.isNATO().toString()); result = ps.executeUpdate(); // Get the ID of the record that was just inserted. ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { id = rs.getInt(1); } } if ((result == PreparedStatement.EXECUTE_FAILED) || (id <= 0)) { this._conn.rollback(sp); this.notifyTableUpdateFailedListeners(new TableUpdateFailedEventArgs(Tables.ChamberCaliber, id)); } this._conn.setAutoCommit(true); this.endUpdate(Tables.ChamberCaliber, start, id); } }