/* * DataAccessLayer * * Copyright (C) 2010 Jaroslav Merxbauer * * 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 3 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, see <http://www.gnu.org/licenses/>. * */ package notwa.dal; import java.sql.ResultSet; import java.util.HashMap; import notwa.common.ConnectionInfo; import notwa.wom.Context; import notwa.wom.BusinessObject; import notwa.exception.DalException; import notwa.sql.SimpleSqlFilter; import notwa.sql.Sql; import notwa.sql.SqlParameterSet; import notwa.sql.SqlBuilder; import notwa.sql.SqlFilter; import notwa.wom.BusinessObjectCollection; import org.apache.log4j.Logger; /** * <code>DataAccessLayer</code> is an abstract class providing a basic functionality to all * inherited <code>DataAccessLayer</code>s. * It is intended to be constructed with given <code>ConnectionInfo</code> and * with given <code>Context</code> within it should live. * <p>It keeps a single instance of connection for every particular <code>ConnectionInfo</code> * so it is not creating two same instances of the same <code>ConnectionInfo</code> * during the life time of the application</p> * <p>The context awarness provides a way how to more efficiently maintain the * access to the database, for the requested entity could have been already requested * before, so it is not neccessary to ask the database for the data and recreate the entity again. * The <code>Context</code> instance hold all entities created within the same context, * in literal meaning.</p> * * @param <TObject> The <code>BusinessObject</code> concrete implementation to * be used as <code>BusinessObject</code> unit being created * by the concrete <code>DataAccessLayer</code>. * * @param <TCollection> The <code>BusinessObjectCollection</code> concrete implementation * to be filled by this concrete <code>DataAccessLayer</code>. * * @author Jaroslav Merxbauer * @version %I% %G% */ public abstract class DataAccessLayer<TObject extends BusinessObject, TCollection extends BusinessObjectCollection<TObject>> { private static HashMap<ConnectionInfo, DatabaseConnection> connections; protected ConnectionInfo ci; protected Context currentContext; protected Logger log; protected DataAccessLayer() { Logger.getLogger(this.getClass()).debug("Creating DataAccessLayer subclass with default constructor!"); } /** * This is actually the default constructor which should be always used to * create an instance of any DAL. * * @param ci The <code>ConnectionInfo</code> which refers the actual database * where we want to collect data from. * @param context The actual <code>Context</code> where we want to let the DAL * live its pittyful life of collectiong data. */ public DataAccessLayer(ConnectionInfo ci, Context context) { this.log = Logger.getLogger(this.getClass()); this.ci = ci; this.currentContext = context; if (connections == null) { connections = new HashMap<ConnectionInfo, DatabaseConnection>(); } if ((ci != null) && !connections.containsKey(ci)) { connections.put(ci, new DatabaseConnection(ci)); } } /** * Retrieves the sole <code>DatabaseConnection</code> assigned to the * <code>ConnectionInfo</code> which was used to construct this instance of * DAL. * * @return The actual <code>DatabaseConnection</code>. */ protected DatabaseConnection getConnection() { return connections.get(ci); } /** * Fills the given <code>BusinessObjectCollection</code> with all possible data. * * @param boc The <code>BusinessObjectCollection</code> to fill. * @return The number of <code>BusinessObjects</code>s filled into the <code>Collection</code>. */ public int fill(TCollection boc) { return fill(boc, new SqlParameterSet()); } /** * Fills the given <code>BusinessObjectCollection</code> with data based on * the given <code>SqlParameterSet</code>. * @see Parameters * * @param boc The <code>BusinessObjectCollection</code> to fill. * @param pc The <code>SqlParameterSet</code> upon which the given collection * will be filled. * @return The number of <code>BusinessObject</code>s filled into the <code>Collection</code>. */ public int fill(TCollection boc, SqlParameterSet pc) { String sql = getSqlTemplate(); SqlFilter filter = new SimpleSqlFilter(pc, Sql.Logical.CONJUNCTION); SqlBuilder sb = new SqlBuilder(sql, filter); try { ResultSet rs = getConnection().executeQuery(sb.compileSql()); boc.setClosed(false); boc.setResultSet(new SmartResultSet(rs, filter)); while (rs.next()) { TObject bo = null; Object pk = getPrimaryKey(rs); if (isInCurrentContext(pk)) { bo = getBusinessObject(pk); } else { bo = getBusinessObject(pk, rs); } if (!boc.add(bo)) { log.debug(String.format("BusinessObject %s could not be added to the collection!", bo.toString())); } } } catch (Exception ex) { log.error("Error occured while filling the Business Object Collection!", ex); } finally { /* * Make sure that the collection knows that it is up-to-date and close * it. This will ensure that any further addition/removal will be properly * remarked! */ boc.setUpdateRequired(false); boc.setClosed(true); } return boc.size(); } /** * Gets the single <code>BusinessObject</code> from the database based on the given * primary key. * If <code>BusinessObject> with the same primary key is already present in * the current <code>Context</code>, it is rather picked up from that context * instead of creating a new one and wasting the time with waiting for the * database to process the query. * * @param primaryKey It should be the primary key of requested <code>BusinessObject</code>. * @return The requested <code>BusinessObject</code> instance. * <code>null</code if there is no such a <code>BusinessObject</code>. * @throws DalException Whenever the given primaryKey actually isn't a * primary key or a database connection issue occures. */ public TObject get(Object primaryKey) throws DalException { if (isInCurrentContext(primaryKey)) { return getBusinessObject(primaryKey); } else { String sql = getSqlTemplate(); SqlParameterSet ps = getPrimaryKeyParams(primaryKey); SqlBuilder sb = new SqlBuilder(sql, new SimpleSqlFilter(ps, Sql.Logical.CONJUNCTION)); try { ResultSet rs = getConnection().executeQuery(sb.compileSql()); if (!rs.last()) { return null; } if (rs.getRow() == 1) { return getBusinessObject(primaryKey, rs); } else { throw new DalException("Supplied parameters are not a primary key!"); } } catch (Exception ex) { throw new DalException(String.format("Unexpected error occured while getting a BusinessObject with primary key %s!", primaryKey.toString()), ex); } } } /** * Updates the database representation of the given concrete implementation * of <code>BusinessObjectCollection</code>. * <p>It is required that the given <code>BusinessObjectCollection</code> * has been built by this DAL, otherwise now action will be taken! Moreover, * it will skip the processing if there have been no changes made.</p> * <p>It utilizes already existing <code>ResultSet</code> used previously to * build the given <code>BusinessObjectCollection</code> to make the actuall * update/insert/delete againts the database.</p> * * @param boc The <code>BusinessObjectCollection</code> which changes should * be mirrored to the database. */ public void update(TCollection boc) { if (boc.getResultSet() == null && !boc.isUpdateRequired()) { return; } SmartResultSet srs = boc.getResultSet(); ResultSet rs = srs.getRs(); try { rs.beforeFirst(); /* * Make sure that all existing rows gets updated */ while (rs.next()) { Object primaryKey = getPrimaryKey(rs); TObject bo = boc.getByPrimaryKey(primaryKey); if (bo.isDeleted()) { rs.deleteRow(); } else if (bo.isUpdated()) { updateSingleRow(srs, bo); rs.updateRow(); } } /* * Proceed with inserting of new rows */ for (TObject bo : boc) { if (bo.isInserted()) { /* * ID higher than 1 mil is virtual, autogenerated, id which * is supposed to be replaced by the valid database id */ if (bo.getUniqeIdentifier() >= 1000000) { bo.setUniqeIdentifier(getLastUniqeIdentifier(bo) + 1); } rs.moveToInsertRow(); updateSingleRow(srs, bo); rs.insertRow(); rs.moveToCurrentRow(); } } /* * Commit when and only when we succesfuly pass the whole update process! */ boc.commit(); } catch (Exception ex) { log.error("Error occured while updating the Business Object Collection"); } } /** * Refresh all the data contained in given <code>BusinessObjectCollection</code>. * At first, it completely clears the given </code>BusinessObjectCollection</code> * by calling the {@link BusinessObjectCollection#shakeAllAway()}. Then it simply * refills the <code>BusinessObjectCollection</code> from the database. * * @param boc The <code>BusinessObjectCollection</code to be refreshed. */ public void refresh(TCollection boc) { refresh(boc, new SqlParameterSet()); } /** * Refresh all the data contained in given <code>BusinessObjectCollection</code>. * At first, it completely clears the given </code>BusinessObjectCollection</code> * by calling the {@link BusinessObjectCollection#shakeAllAway()}. Then it simply * refills the <code>BusinessObjectCollection</code> from the database taking * in account the given <coded>SqlParameterSet</code>. * * @param boc The <code>BusinessObjectCollection</code to be refreshed. * @param ps The <code>SqlParameterSet</code> upon which the given collection * will be refreshed. */ public void refresh(TCollection boc, SqlParameterSet ps) { boc.shakeAllAway(); fill(boc, ps); } /** * Acquires current highest valid uniqe identifier which is valid within the * database. * * @param bo The <code>BusinessObject</code> we are looking the uniqe identifier * for. * @return The last valid value of uniqe identifier. * @throws DalException Whenever the acquired sql query actually isn't valid * or a database connection issue occures. */ protected Integer getLastUniqeIdentifier(TObject bo) throws DalException { try { Integer potentialPrimaryKey = getConnection().executeScalar(getHighestUniqeIdentifierSql(bo)); return (potentialPrimaryKey == null) ? 0 : potentialPrimaryKey; } catch (Exception ex) { throw new DalException(String.format("Unexpected error occured while getting a last uniqe identifier for %s!", bo.toString()), ex); } } /** * Gets the parametrized sql template which shall be provided by every concrete * implmentation of the DataAccessLayer. * <p>This sql actually describes how to obtain the concrete data from the * database and its used to build the <code>RecordSet</code> which is then * used to build the concrete <code>BusinessObjectCollection</code</p> and * the concrete implementation should know how it wants to build its data. * * @return The actuall sql parametrized template to fill the concrete <code> * BusinessObjectCollection</code> implementation. */ protected abstract String getSqlTemplate(); /** * Parses the concrete <code>BusinessObject</code> from the given <code>ResultSet</code>. * <p>This method is called only if the concrete <code>BusinessObject</code> has * not been found wihtin a current <code>Context</code> and the concrete * implementation should know how to obtain the requested data from the given * <code>ResultSet</code> * * @param primaryKey The actuall primarky key identyfying the concrete * implementation of <code>BusinessObject</code> * * @param rs <code>ResultSet</code> where the data will be parsed from. * @return Built concrete implementation of <code>BusinessObject</code>. * @throws DalException Whenever error occures during the <code>ResultSet</code> * parsing, which should indicate that there is a column * missing in the database. This should be a rare occurance. */ protected abstract TObject getBusinessObject(Object primaryKey, ResultSet rs) throws DalException; /** * Gets the concrete <code>BusinessObject</code> from the actual <code>Context</code>, * if there is one. * <p>This method always preceedes the {@link #getBusinessObject(java.lang.Object, java.sql.ResultSet)} * if the current <code>Context</code> contains the <code>BusinessObject</code> * identyfied by its primary key and the concrete implementation should know * how to obtain its concrete <code>BusinessObject</cpdo> from <code>Context</code>.</p> * * @param primaryKey The actuall primary key identyfying the concrete * implementation of <code>BusinessObject</code>. * @return Concrete implementation of <code>BusinessObject</code> acquired from * the current <code>Context</code>. * @throws DalException Whenever error occures during the primary key recognition, * which should point to incorrect parameter passing. The * caller probably passed the primary key represented as * an unexpected datatype. */ protected abstract TObject getBusinessObject(Object primaryKey) throws DalException; /** * Parses the primary key of the concrete <code>BusinessObject</code> from * the given <code>ResultSet</code>. * * @param rs The <code>ResultSet</code> where the primary key shall be parsed * from. The concrete implementation knows what is/are the primary * key column(s). * @return The parsed primary key. * @throws DalException Whenever the concrete implementation doesn't find the * expected columns in the given <code>ResultSet</code>. */ protected abstract Object getPrimaryKey(ResultSet rs) throws DalException; /** * Checks whether the concrete implementation of <code>BusinessObject</code> * identyfied by the given primary key is not already present in the current * <code>Context</code>. * <p>The concrete implementation should know how to query the <code>Context</code> * for the concrete implementation of <code>BusinessObject</code>.</p> * * @param primaryKey The actuall primary key identyfying the concrete * implementation of <code>BusinessObject</code>. * @return <code>true</code> if the queried concrete <code>BusinessObject</code> * is already maintained within the current <code>Context</code>, * <code>false</code> otherwise. * @throws DalException Whenever the concrete implementation doesn't find the * expected columns in the given <code>ResultSet</code>. */ protected abstract boolean isInCurrentContext(Object primaryKey) throws DalException; /** * Builds the <code>SqlParameterSet</code> which will then identify the one and * only concrete <code>BusinessObject</code> when there is a need to query * such a <code>BusinessObject</code> from the database. * <p>The concrete implementation shoudl know how exactly build the <code>SqlParameterSet</code> * using the given primary key.</p> * @param primaryKey The actuall primary key identyfying the concrete * implementation of <code>BusinessObject</code>. * @return The built <code>SqlParameterSet</code> which could then by used to * obtain the one and only <code>BusinessObject</code> from database. */ protected abstract SqlParameterSet getPrimaryKeyParams(Object primaryKey); /** * Updates the single row in the given <code>ResultSet</code> based on the * given concrete implementation of <code>BusinessObject</code>. * <p>The concrete implementation should know which columns should be updated * based on which values from the given <code>BusinessObject</code>.</p> * * @param rs The <code>ResultSet</code> to be updated. * @param bo The <code>BusinessObject</code> to be used to update the given * <code>ResultSet</code>. * @throws Exception Whenever the concrete implementation doesn't find the * expected columns in the given <code>ResultSet</code>. */ protected abstract void updateSingleRow(SmartResultSet rs, TObject bo) throws Exception; /** * Gets the sql query that should return the current highest value of uniqe * identifier. * The query pattern should look like the following example: * <code>SELECT primary_key_column FROM table SORT BY primary_key_column DESC</code> * * @param bo <code>BusinessObject</code> for which we are building the query. * @return The requested query. */ protected abstract String getHighestUniqeIdentifierSql(TObject bo); }