org.cyrusbuilt.guncabinet.dao.DbEngine.java Source code

Java tutorial

Introduction

Here is the source code for org.cyrusbuilt.guncabinet.dao.DbEngine.java

Source

/**
 * 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);
    }
}