/* * jPOS Project [http://jpos.org] * Copyright (C) 2000-2017 jPOS Software SRL * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.jpos.gl; import java.util.*; import java.math.BigDecimal; import org.hibernate.*; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.hibernate.criterion.Disjunction; import org.hibernate.exception.ConstraintViolationException; import org.hibernate.type.LongType; import org.jpos.ee.DB; /** * MiniGL facility entry point. * * @author <a href="mailto:apr@jpos.org">Alejandro Revilla</a> */ public class GLSession { private static Map<String, Object> ruleCache = new HashMap<String, Object>(); private GLUser user; private Session session; private DB db; public static final short[] LAYER_ZERO = new short[] { 0 }; public static final BigDecimal ZERO = new BigDecimal ("0.00"); public static final BigDecimal Z = new BigDecimal ("0"); private long SAFE_WINDOW = 1000L; /** * Construct a GLSession for a given user. * * User has to exist in MiniGL gluser table unless username is null. * @see GLUser * * @param username the user name. * @throws org.hibernate.HibernateException on database problems * @throws GLException if user is not valid */ public GLSession (String username) throws HibernateException, GLException { super(); this.db = new DB(); session = db.open(); if (username != null) { user = getUser (username); if (user == null) { close(); throw new GLException ("Invalid user '" + username + "'"); } } } /** * Construct a GLSession using property <code>user.name</code>. * User has to exist in MiniGL gluser table. * @see GLUser * @throws org.hibernate.HibernateException on database problems * @throws GLException if user.name is not valid */ public GLSession () throws HibernateException, GLException { this (System.getProperty ("user.name")); } /** * Construct a GLSession for a given user. * * User has to exist in MiniGL gluser table unless username is null. * @see GLUser * * @param db EE DB * @param username the user name. * @throws HibernateException on database related issue * @throws GLException if user is invalid */ public GLSession (DB db, String username) throws HibernateException, GLException { super(); this.db = db; boolean autoClose = false; if (db.session() == null) { db.open(); autoClose = !autoClose; } session = db.session(); if (username != null) { user = getUser (username); if (user == null) { if (autoClose) close(); throw new GLException ("Invalid user '" + username + "'"); } } } /** * Construct a GLSession using property <code>user.name</code>. * User has to exist in MiniGL gluser table. * @param db EE DB * @see GLUser * @throws org.hibernate.HibernateException on hibernate exception * @throws GLException on GL level exception */ public GLSession (DB db) throws HibernateException, GLException { this (db, System.getProperty ("user.name")); } /** * @param action name * @return true if user has permission to perform given action * @see GLPermission */ public boolean hasPermission (String action) { Iterator iter = user.getPermissions().iterator(); while (iter.hasNext()) { GLPermission p = (GLPermission) iter.next(); if (p.getJournal() == null && action.equals (p.getName())) return true; } return false; } /** * @param action name * @throws GLException if user doesn't have permission. * @see GLPermission */ public void checkPermission (String action) throws GLException { if (!hasPermission (action)) { throw new GLException ( "User '" + user.getName() + "' (" + user.getId() + ") does not have '" + action + "' permission." ); } } /** * Grant permission to user. * In order to grant a permission, we need to have both the permission * and GRANT. * * @param userName user name * @param permName permission name */ public void grant (String userName, String permName) throws GLException, HibernateException { checkPermission (GLPermission.GRANT); checkPermission (permName); GLPermission perm = new GLPermission(permName); session.save (perm); GLUser u = getUser (userName); u.grant (perm); } /** * Revoke permission from user. * In order to grant a permission, we need to have both the permission * and GRANT. * * @param userName user name * @param permName permission name */ public void revoke (String userName, String permName) throws GLException, HibernateException { checkPermission (GLPermission.GRANT); GLUser u = getUser (userName); u.revoke (permName); } /** * Grant permission to user no matter if we have the premission nor GRANT. * * @param userName user name * @param permName permission name */ public void forceGrant (String userName, String permName) { GLPermission perm = new GLPermission(permName); session.save (perm); GLUser u = getUser (userName); u.grant (perm); } /** * Revoke permission from user no matter if we have the permission nor GRANT. * * @param userName user name * @param permName permission name */ public void forceRevoke (String userName, String permName) { GLUser u = getUser (userName); u.revoke (permName); } /** * Verifies user's permission in a given journal. * @param action name * @param j journal * @return true if user has permission to perform given action. * @see GLPermission * @see Journal */ public boolean hasPermission (String action, Journal j) { return getUser().hasPermission(action, j); } /** * Check user's permission in a given journal. * @param action name * @param j journal * @throws GLException if user doesn't have permission. * @see GLPermission * @see Journal */ public void checkPermission (String action, Journal j) throws GLException { if (!hasPermission (action, j)) { throw new GLException ( "User '" + user.getName() + "' (" + user.getId() + ") does not have '" + action + "' permission in journal '" + j.getName() + "' (" + j.getId() + ")" ); } } /** * @param code chart of account's code * @return top level chart with given code or null. * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public Account getChart (String code) throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery ( "from acct in class org.jpos.gl.CompositeAccount where code=:code and parent is null" ); q.setParameter ("code", code); Iterator iter = q.list().iterator(); return (Account) (iter.hasNext() ? iter.next() : null); } /** * @return List of charts of accounts. * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public List<Account> getCharts () throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery( "from acct in class org.jpos.gl.CompositeAccount where parent is null" ); return q.list(); } /** * @param chart chart of accounts. * @param code account's code. * @return account with given code in given chart, or null. * * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public Account getAccount (Account chart, String code) throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery ( "from acct in class org.jpos.gl.Account where root=:chart and code=:code" ); q.setLong ("chart", chart.getId()); q.setParameter ("code", code); Iterator iter = q.list().iterator(); return (Account) (iter.hasNext() ? iter.next() : null); } /** * Add account to parent. * Check permissions, parent's type and optional currency. * * @param parent parent account * @param acct account to add * @throws HibernateException on error * @throws GLException if user doesn't have permissions, or type mismatch */ public void addAccount (CompositeAccount parent, Account acct) throws HibernateException, GLException { addAccount (parent, acct, false); } /** * Add account to parent. * Check permissions, parent's type and optional currency. * * @param parent parent account * @param acct account to add * @param fast true if we want a fast add that do not eagerly load all childrens * @throws HibernateException on error * @throws GLException if user doesn't have permissions, type mismatch or Duplicate Code */ @SuppressWarnings("unchecked") public void addAccount (CompositeAccount parent, Account acct, boolean fast) throws HibernateException, GLException { checkPermission (GLPermission.WRITE); if (!parent.isChart() && !parent.equalsType (acct)) { StringBuffer sb = new StringBuffer ("Type mismatch "); sb.append (parent.getTypeAsString()); sb.append ('/'); sb.append (acct.getTypeAsString()); throw new GLException (sb.toString()); } String currencyCode = parent.getCurrencyCode(); if (currencyCode != null && !currencyCode.equals (acct.getCurrencyCode())) { StringBuffer sb = new StringBuffer ("Currency mismatch "); sb.append (currencyCode); sb.append ('/'); sb.append (acct.getCurrencyCode()); throw new GLException (sb.toString()); } acct.setRoot (parent.getRoot()); try { session.save (acct); session.flush(); } catch (ConstraintViolationException e) { e.fillInStackTrace(); throw new GLException("Duplicate code", e); } acct.setParent (parent); if (!fast) parent.getChildren().add (acct); } /** * Add a chart of accounts. * Check permissions. * * @param acct chart to add * @throws HibernateException on error * @throws GLException if user doesn't have write permission */ public void addChart (Account acct) throws HibernateException, GLException { checkPermission (GLPermission.WRITE); session.save (acct); } /** * @param chart chart of accounts. * @param code account's code. * @return final account with given code in given chart, or null. * * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public FinalAccount getFinalAccount (Account chart, String code) throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery ( "from acct in class org.jpos.gl.FinalAccount where root=:chart and code=:code" ); q.setLong ("chart", chart.getId()); q.setParameter ("code", code); Iterator iter = q.list().iterator(); return (FinalAccount) (iter.hasNext() ? iter.next() : null); } /** * @param chart chart of accounts. * @return list of final accounts * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public List<FinalAccount> getFinalAccounts (Account chart) throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery ( "from acct in class org.jpos.gl.FinalAccount where root=:chart" ); q.setLong ("chart", chart.getId()); return (List<FinalAccount>) q.list(); } /** * @param parent parent account. * @return list of composite accounts children of the parent account * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public List<CompositeAccount> getCompositeChildren (Account parent) throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery( "from acct in class org.jpos.gl.CompositeAccount where parent=:parent" ); q.setParameter ("parent", parent); return (List<CompositeAccount>) q.list(); } /** * @param parent parent account. * @return list of composite accounts children of the parent account * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public List<FinalAccount> getFinalChildren (Account parent) throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery( "from acct in class org.jpos.gl.FinalAccount where parent=:parent" ); q.setParameter ("parent", parent); return (List<FinalAccount>) q.list(); } /** * @param chart chart of accounts. * @return list of all accounts * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public List<Account> getAllAccounts (Account chart) throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery ( "from acct in class org.jpos.gl.Account where root=:chart" ); q.setLong ("chart", chart.getId()); return (List<Account>) q.list(); } /** * @param chart chart of accounts. * @param code account's code. * @return composite account with given code in given chart, or null. * * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public CompositeAccount getCompositeAccount (Account chart, String code) throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery ( "from acct in class org.jpos.gl.CompositeAccount where root=:chart and code=:code" ); q.setLong ("chart", chart.getId()); q.setParameter ("code", code); Iterator iter = q.list().iterator(); return (CompositeAccount) (iter.hasNext() ? iter.next() : null); } /** * @param chartName chart of account's code. * @param code account's code. * @return account with given code in given chart, or null. * * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public Account getAccount (String chartName, String code) throws HibernateException, GLException { Account chart = getChart(chartName); if (chart == null) throw new GLException ("Chart '" + chartName + "' does not exist"); return getAccount(chart, code); } /** * @param chartName chart of account's code. * @param code account's code. * @return final account with given code in given chart, or null. * * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public FinalAccount getFinalAccount (String chartName, String code) throws HibernateException, GLException { Account chart = getChart(chartName); if (chart == null) throw new GLException ("Chart '" + chartName + "' does not exist"); return getFinalAccount (chart, code); } /** * @param chartName chart of account's code. * @param code account's code. * @return composite account with given code in given chart, or null. * * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public CompositeAccount getCompositeAccount (String chartName, String code) throws HibernateException, GLException { Account chart = getChart(chartName); if (chart == null) throw new GLException ("Chart '" + chartName + "' does not exist"); return getCompositeAccount (chart, code); } /** * @param name journal's name. * @return journal or null. * @throws GLException if users doesn't have global READ permission. * @throws HibernateException on database errors. * @see GLPermission */ public Journal getJournal (String name) throws HibernateException, GLException { Query q = session.createQuery ( "from journal in class org.jpos.gl.Journal where name=:name" ); q.setParameter ("name", name); Iterator iter = q.list().iterator(); Journal j = iter.hasNext() ? (Journal) iter.next() : null; if (j == null) throw new GLException ("Journal '" + name + "' does not exist"); checkPermission (GLPermission.READ, j); return j; } /** * @return list of all journals * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. * @see GLPermission */ public List<Journal> getAllJournals () throws HibernateException, GLException { checkPermission (GLPermission.READ); Query q = session.createQuery ( "from acct in class org.jpos.gl.Journal order by chart" ); return (List<Journal>) q.list(); } /** * @return list of all currency ids * @see org.jpos.gl.Currency */ public List<String> getCurrencyCodes() { return db.session() .createCriteria(Currency.class) .setProjection(Projections.id()) .list(); } /** * Post transaction in a given journal. * * @param journal the journal. * @param txn the transaction. * @throws GLException if user doesn't have POST permission or any rule associated with this journal and/or account raises a GLException. * @throws HibernateException on database errors. * @see GLPermission * @see JournalRule */ public void post (Journal journal, GLTransaction txn) throws HibernateException, GLException { checkPermission (GLPermission.POST, journal); txn.setJournal (journal); txn.setTimestamp (new Date()); if (txn.getPostDate() == null) txn.setPostDate (txn.getTimestamp()); else invalidateCheckpoints (txn); Collection rules = getRules (txn); // dumpRules (rules); applyRules (txn, rules); session.save (txn); } /** * Moves a transaction to a new journal * @param txn the Transaction * @param journal the New Journal * @throws GLException if user doesn't have POST permission on the old and new journals. * @throws HibernateException on database errors. */ public void move (GLTransaction txn, Journal journal) throws GLException, HibernateException { checkPermission (GLPermission.POST, journal); checkPermission (GLPermission.POST, txn.getJournal()); invalidateCheckpoints (txn); // invalidate in old journal txn.setJournal (journal); invalidateCheckpoints (txn); // invalidate in new journal applyRules (txn, getRules (txn)); session.update (txn); } /** * Summarize transactions in a journal. * * @param journal the journal. * @param start date (inclusive). * @param end date (inclusive). * @param description summary transaction's description * @return GLTransaction a summary transaction * @throws GLException if user doesn't have READ permission on this journal. * @throws HibernateException on database/mapping errors */ public GLTransaction summarize (Journal journal, Date start, Date end, String description, short[] layers) throws HibernateException, GLException { checkPermission (GLPermission.SUMMARIZE, journal); start = Util.floor (start); end = Util.ceil (end); if (end.compareTo (start) < 0) { throw new GLException ("Invalid date range " + Util.dateToString(start) + ":" + Util.dateToString (end)); } Date lockDate = journal.getLockDate(); if (lockDate != null && start.compareTo (lockDate) <= 0) { throw new GLException ("Journal is locked at " + Util.dateToString (lockDate)); } setLockDate (journal, end); GLTransaction txn = new GLTransaction (description); for (int i=0; i<layers.length; i++) { Iterator debits = findSummarizedGLEntries (journal, start, end, false, layers[i]); Iterator credits = findSummarizedGLEntries (journal, start, end, true, layers[i]); while (debits.hasNext()) { Object[] obj = (Object[]) debits.next(); txn.createDebit ( (FinalAccount) obj[0], (BigDecimal) obj[1], null, layers[i] ); } while (credits.hasNext()) { Object[] obj = (Object[]) credits.next(); txn.createCredit ( (FinalAccount) obj[0], (BigDecimal) obj[1], null, layers[i] ); } } txn.setJournal (journal); txn.setTimestamp (new Date()); txn.setPostDate (end); deleteGLTransactions (journal, start, end); session.save (txn); // force post - no rule validations journal.setLockDate (null); return txn; } /** * @param journal the journal. * @param id txn id * @return GLTransaction or null * @throws GLException if user doesn't have READ permission on this journal. */ public GLTransaction getTransaction (Journal journal, long id) throws HibernateException, GLException { GLTransaction txn = null; checkPermission (GLPermission.READ, journal); try { txn = (GLTransaction) session.load (GLTransaction.class, new Long(id)); if (!txn.getJournal().equals(journal)) throw new GLException ( "The transaction does not belong to the specified journal" ); } catch (ObjectNotFoundException e) { // okay to happen } return txn; } /** * @param journal the journal. * @param start date (inclusive). * @param end date (inclusive). * @param searchString optional search string * @param findByPostDate true to find by postDate, false to find by timestamp * @param pageNumber the page number * @param pageSize the page size * @return list of transactions * @throws GLException if user doesn't have READ permission on this journal. */ public Criteria createFindTransactionsCriteria (Journal journal, Date start, Date end, String searchString, boolean findByPostDate, int pageNumber, int pageSize) throws HibernateException, GLException { int firstResult = 0; if (pageSize > 0 && pageNumber > 0) firstResult = pageSize * (pageNumber - 1); return createFindTransactionsCriteriaByRange( journal, start, end, searchString, findByPostDate, firstResult, pageSize ); } /** * @param journal the journal. * @param start date (inclusive). * @param end date (inclusive). * @param searchString optional search string * @param findByPostDate true to find by postDate, false to find by timestamp * @param firstResult the first result * @param pageSize the page size * @return list of transactions * @throws GLException if user doesn't have READ permission on this journal. */ public Criteria createFindTransactionsCriteriaByRange (Journal journal, Date start, Date end, String searchString, boolean findByPostDate, int firstResult, int pageSize) throws HibernateException, GLException { checkPermission (GLPermission.READ, journal); String dateField = findByPostDate ? "postDate" : "timestamp"; if (findByPostDate) { if (start != null) start = Util.floor (start); if (end != null) end = Util.ceil (end); } Criteria crit = session.createCriteria (GLTransaction.class) .add (Restrictions.eq ("journal", journal)); if (start != null && start.equals (end)) crit.add (Restrictions.eq (dateField, start)); else { if (start != null) crit.add (Restrictions.ge (dateField, start)); if (end != null) crit.add (Restrictions.le (dateField, end)); } if (searchString != null) crit.add (Restrictions.like ("detail", "%" + searchString + "%")); if (pageSize > 0 && firstResult > 0) { crit.setMaxResults (pageSize); crit.setFirstResult (firstResult); } return crit; } /** * @param journal the journal. * @param start date (inclusive). * @param end date (inclusive). * @param searchString optional search string * @param findByPostDate true to find by postDate, false to find by timestamp * @param pageNumber the page number * @param pageSize the page size * @return list of transactions * @throws GLException if user doesn't have READ permission on this journal. */ public List findTransactions (Journal journal, Date start, Date end, String searchString, boolean findByPostDate, int pageNumber, int pageSize) throws HibernateException, GLException { return createFindTransactionsCriteria (journal, start, end, searchString, findByPostDate, pageNumber, pageSize).list(); } /** * @param journal the journal. * @param start date (inclusive). * @param end date (inclusive). * @param searchString optional search string * @param findByPostDate true to find by postDate, false to find by timestamp * @return list of transactions * @throws GLException if user doesn't have READ permission on this journal. */ public List findTransactions (Journal journal, Date start, Date end, String searchString, boolean findByPostDate) throws HibernateException, GLException { return findTransactions (journal, start, end, searchString, findByPostDate, 0, 0); } /** * @param journal the journal. * @param start date (inclusive). * @param end date (inclusive). * @param searchString optional search string * @param findByPostDate true to find by postDate, false to find by timestamp * @return list of transactions' ids * @throws GLException if user doesn't have READ permission on this journal. */ public List findTransactionsIds (Journal journal, Date start, Date end, String searchString, boolean findByPostDate, int pageNumber, int pageSize) throws HibernateException, GLException { checkPermission (GLPermission.READ, journal); String dateField = findByPostDate ? "postDate" : "timestamp"; if (findByPostDate) { if (start != null) start = Util.floor (start); if (end != null) end = Util.ceil (end); } Criteria crit = session.createCriteria (GLTransaction.class) .add (Restrictions.eq ("journal", journal)); crit.setProjection(Projections.id()); if (start != null && start.equals (end)) crit.add (Restrictions.eq (dateField, start)); else { if (start != null) crit.add (Restrictions.ge (dateField, start)); if (end != null) crit.add (Restrictions.le (dateField, end)); } if (searchString != null) crit.add (Restrictions.like ("detail", "%" + searchString + "%")); if (pageSize > 0 && pageNumber > 0) { crit.setMaxResults (pageSize); crit.setFirstResult (pageSize * (pageNumber - 1)); } return crit.list(); } /** * @param journal the journal. * @param start date (inclusive). * @param end date (inclusive). * @param searchString optional search string * @param findByPostDate true to find by postDate, false to find by timestamp * @return number of transactions * @throws GLException if user doesn't have READ permission on this journal. */ public Long findTransactionsRowCount (Journal journal, Date start, Date end, String searchString, boolean findByPostDate) throws HibernateException, GLException { checkPermission (GLPermission.READ, journal); String dateField = findByPostDate ? "postDate" : "timestamp"; if (findByPostDate) { if (start != null) start = Util.floor (start); if (end != null) end = Util.ceil (end); } Criteria crit = session.createCriteria (GLTransaction.class) .add (Restrictions.eq ("journal", journal)); crit.setProjection(Projections.rowCount()); if (start != null && start.equals (end)) crit.add (Restrictions.eq (dateField, start)); else { if (start != null) crit.add (Restrictions.ge (dateField, start)); if (end != null) crit.add (Restrictions.le (dateField, end)); } if (searchString != null) crit.add (Restrictions.like ("detail", "%" + searchString + "%")); return (Long)crit.uniqueResult(); } /** * @return user object associated with this session. */ public GLUser getUser() { return user; } /** * Current Balance for account in a given journal. * @param journal the journal. * @param acct the account. * @return current balance. * @throws GLException if user doesn't have READ permission on this jounral. */ public BigDecimal getBalance (Journal journal, Account acct) throws HibernateException, GLException { return getBalances (journal, acct, null, true) [0]; } /** * Current Balance for account in a given journal. * @param journal the journal. * @param acct the account. * @param layer the layers. * @return current balance. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal getBalance (Journal journal, Account acct, short layer) throws HibernateException, GLException { return getBalances (journal, acct, null, true, new short[] { layer }, 0L) [0]; } /** * Current Balance for account in a given journal. * @param journal the journal. * @param acct the account. * @param layers the layers. * @return current balance. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal getBalance (Journal journal, Account acct, short[] layers) throws HibernateException, GLException { return getBalances (journal, acct, null, true, layers, 0L) [0]; } /** * Current Balance for account in a given journal. * @param journal the journal. * @param acct the account. * @param layers comma separated list of layers * @return current balance. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal getBalance (Journal journal, Account acct, String layers) throws HibernateException, GLException { return getBalances (journal, acct, null, true, toLayers(layers), 0L) [0]; } /** * Balance for account in a given journal in a given date. * @param journal the journal. * @param acct the account. * @param date date (inclusive). * @return balance at given date. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal getBalance (Journal journal, Account acct, Date date) throws HibernateException, GLException { return getBalances (journal, acct, date, true) [0]; } /** * Balance for account in a given journal in a given date. * @param journal the journal. * @param acct the account. * @param date date (inclusive). * @param layer layer * @return balance at given date. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal getBalance (Journal journal, Account acct, Date date, short layer) throws HibernateException, GLException { return getBalances (journal, acct, date, true, new short[] { layer }, 0L) [0]; } /** * Balance for account in a given journal in a given date. * @param journal the journal. * @param acct the account. * @param date date (inclusive). * @param layers layers * @return balance at given date. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal getBalance (Journal journal, Account acct, Date date, short[] layers) throws HibernateException, GLException { return getBalances (journal, acct, date, true, layers, 0L) [0]; } /** * Balance for account in a given journal in a given date. * @param journal the journal. * @param acct the account. * @param date date (inclusive). * @param layers comma separated list of layers * @return balance at given date. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal getBalance (Journal journal, Account acct, Date date, String layers) throws HibernateException, GLException { return getBalances (journal, acct, date, true, toLayers(layers), 0L) [0]; } /** * Get Both Balances at given date * @param journal the journal. * @param acct the account. * @param date date (inclusive). * @param inclusive either true or false * @return array of 2 BigDecimals with balance and entry count. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal[] getBalances (Journal journal, Account acct, Date date, boolean inclusive) throws HibernateException, GLException { return getBalances (journal, acct, date, inclusive, LAYER_ZERO, 0L); } /** * Get Both Balances at given date * @param journal the journal. * @param acct the account. * @param date date (inclusive). * @param inclusive either true or false * @param layers the layers * @param maxId maximum GLEntry ID to be considered in the query (if greater than zero) * @return array of 2 BigDecimals with balance and entry count. * @throws GLException if user doesn't have READ permission on this journal. */ public BigDecimal[] getBalances (Journal journal, Account acct, Date date, boolean inclusive, short[] layers, long maxId) throws HibernateException, GLException { checkPermission (GLPermission.READ, journal); BigDecimal balance[] = { ZERO, Z }; short[] layersCopy = Arrays.copyOf(layers,layers.length); if (acct.getChildren() != null) { if (acct.isChart()) { return getChartBalances (journal, (CompositeAccount) acct, date, inclusive, layersCopy, maxId); } Iterator iter = acct.getChildren().iterator(); while (iter.hasNext()) { Account a = (Account) iter.next(); BigDecimal[] b = getBalances (journal, a, date, inclusive, layersCopy, maxId); balance[0] = balance[0].add (b[0]); // session.evict (a); FIXME this conflicts with r251 (cascade=evict genearting a failed to lazily initialize a collection } } else if (acct.isFinalAccount()) { Criteria entryCrit = session.createCriteria (GLEntry.class) .add (Restrictions.eq ("account", acct)) .add (Restrictions.in ("layer", (Object[])toShortArray (layersCopy))); if (maxId > 0L) entryCrit.add (Restrictions.le ("id", maxId)); Criteria txnCrit = entryCrit.createCriteria ("transaction") .add (Restrictions.eq ("journal", journal)); if (date != null) { if (inclusive) { txnCrit.add (Restrictions.lt ("postDate", Util.tomorrow (date))); } else { date = Util.floor (date); txnCrit.add (Restrictions.lt ("postDate", date)); } Checkpoint chkp = getRecentCheckpoint (journal, acct, date, inclusive, layersCopy); if (chkp != null) { balance[0] = chkp.getBalance(); txnCrit.add (Restrictions.gt ("postDate", chkp.getDate())); } } else { BalanceCache bcache = getBalanceCache (journal, acct, layersCopy); if (bcache != null && bcache.getRef() <= maxId) { balance[0] = bcache.getBalance(); entryCrit.add (Restrictions.gt("id", bcache.getRef())); } } List l = txnCrit.list(); balance[0] = applyEntries (balance[0], l); balance[1] = new BigDecimal (l.size()); // hint for checkpoint } return balance; } /** * AccountDetail for date range * @param journal the journal. * @param acct the account. * @param start date (inclusive). * @param end date (inclusive). * @return Account detail for given period. * @throws GLException if user doesn't have READ permission on this journal. */ public AccountDetail getAccountDetail (Journal journal, Account acct, Date start, Date end, short[] layers) throws HibernateException, GLException { checkPermission (GLPermission.READ); start = Util.floor (start); end = Util.ceil (end); Criteria crit = session.createCriteria (GLEntry.class); boolean hasChildren = false; if (acct.isCompositeAccount()) { Disjunction dis = Restrictions.disjunction(); for (Long l : getChildren (acct)) { hasChildren = true; dis.add (Restrictions.idEq(l)); } if (hasChildren) { Criteria subCrit = crit.createCriteria(("account")); subCrit.add (dis); } } if (!hasChildren) { crit.add (Restrictions.eq ("account", acct)); } crit.add (Restrictions.in ("layer", (Object[])toShortArray (layers))); crit = crit.createCriteria ("transaction") .add (Restrictions.eq ("journal", journal)) .add (Restrictions.ge ("postDate", start)) .add (Restrictions.le ("postDate", end)); BigDecimal initialBalance[] = getBalances (journal, acct, start, false, layers, 0L); crit.addOrder (Order.asc ("postDate")); crit.addOrder (Order.asc ("timestamp")); crit.addOrder (Order.asc ("id")); List entries = crit.list(); // BigDecimal finalBalance = applyEntries (initialBalance[0], entries); return new AccountDetail ( journal, acct, initialBalance[0], start, end, entries, layers ); } /** * AccountDetail for date range * @param journal the journal. * @param acct the account. * @param layers layer set * @param maxResults number of entries in mini statement * @return Account detail for given period. * @throws GLException if user doesn't have READ permission on this journal. */ public AccountDetail getMiniStatement (Journal journal, Account acct, short[] layers, int maxResults) throws HibernateException, GLException { checkPermission (GLPermission.READ); Criteria crit = session.createCriteria (GLEntry.class); boolean hasChildren = false; if (acct.isCompositeAccount()) { Disjunction dis = Restrictions.disjunction(); for (Long l : getChildren (acct)) { hasChildren = true; dis.add (Restrictions.idEq(l)); } if (hasChildren) { Criteria subCrit = crit.createCriteria(("account")); subCrit.add (dis); } } if (!hasChildren) { crit.add (Restrictions.eq ("account", acct)); } crit.add (Restrictions.in ("layer", (Object[])toShortArray (layers))); crit = crit.createCriteria ("transaction") .add (Restrictions.eq ("journal", journal)); crit.addOrder (Order.desc ("id")); crit.setMaxResults(maxResults); List<GLEntry> entries = crit.list(); BigDecimal balance = ZERO; if (entries.size() > 0) balance = getBalances(journal, acct, (Date) null, true, layers, entries.get(0).getId())[0]; return new AccountDetail(journal, acct, balance, entries, layers); } /** * @param journal the journal. * @param acct the account. * @param date date (null for last checkpoint) * @param inclusive either true or false * @return Most recent check point for given date. * @throws GLException if user doesn't have CHECKPOINT permission on this journal. */ public Checkpoint getRecentCheckpoint (Journal journal, Account acct, Date date, boolean inclusive, short[] layers) throws HibernateException, GLException { checkPermission (GLPermission.CHECKPOINT, journal); Criteria crit = session.createCriteria (Checkpoint.class) .add (Restrictions.eq ("journal", journal)) .add (Restrictions.eq ("account", acct)); if (layers != null) crit.add (Restrictions.eq ("layers", layersToString(layers))); if (date != null) { if (inclusive) crit.add (Restrictions.le ("date", date)); else crit.add (Restrictions.lt ("date", date)); } crit.addOrder (Order.desc ("date")); crit.setMaxResults (1); return (Checkpoint) crit.uniqueResult(); } public BalanceCache getBalanceCache (Journal journal, Account acct, short[] layers) throws HibernateException, GLException { checkPermission (GLPermission.CHECKPOINT, journal); Criteria crit = session.createCriteria (BalanceCache.class) .add (Restrictions.eq ("journal", journal)) .add (Restrictions.eq ("account", acct)); if (layers != null) crit.add (Restrictions.eq ("layers", layersToString(layers))); crit.addOrder (Order.desc ("ref")); crit.setMaxResults (1); return (BalanceCache) crit.uniqueResult(); } /** * @param journal the Journal * @param acct the account * @param date checkpoint date (inclusive) * @param threshold minimum number of GLEntries required to create a checkpoint * @throws GLException if user doesn't have CHECKPOINT permission on this journal. */ public void createCheckpoint (Journal journal, Account acct, Date date, int threshold) throws HibernateException, GLException { createCheckpoint(journal, acct, date, threshold, LAYER_ZERO); } /** * @param journal the Journal * @param acct the account * @param date checkpoint date (inclusive) * @param layers taken into account in this checkpoint * @param threshold minimum number of GLEntries required to create a checkpoint * @throws GLException if user doesn't have CHECKPOINT permission on this journal. */ public void createCheckpoint (Journal journal, Account acct, Date date, int threshold, short[] layers) throws HibernateException, GLException { if (date == null) throw new GLException ("Invalid checkpoint date"); checkPermission (GLPermission.CHECKPOINT, journal); // Transaction tx = session.beginTransaction(); session.buildLockRequest(LockOptions.UPGRADE).lock(journal); createCheckpoint0 (journal, acct, date, threshold, layers); // tx.commit(); } public BigDecimal createBalanceCache (Journal journal, Account acct, short[] layers) throws HibernateException, GLException { return createBalanceCache (journal, acct, layers, getSafeMaxGLEntryId()); } private BigDecimal createBalanceCache (Journal journal, Account acct, short[] layers, long maxId) throws HibernateException, GLException { BigDecimal balance; if (acct.isCompositeAccount()) { balance = ZERO; Iterator iter = ((CompositeAccount) acct).getChildren().iterator(); while (iter.hasNext()) { Account a = (Account) iter.next(); balance = balance.add (createBalanceCache (journal, a, layers, maxId)); } } else if (acct.isFinalAccount()) { lock (journal, acct); balance = getBalances (journal, acct, null, true, layers, maxId) [0]; BalanceCache c = getBalanceCache (journal, acct, layers); if (c == null) { c = new BalanceCache (); c.setJournal (journal); c.setAccount (acct); c.setLayers (layersToString(layers)); } if (maxId != c.getRef()) { c.setRef (maxId); c.setBalance (balance); session.saveOrUpdate (c); } } return getBalance(journal, acct, layers); } /** * Lock a journal. * @param journal the journal. * @throws HibernateException on database errors. * @throws GLException if user doesn't have POST permission on this journal. */ public void lock (Journal journal) throws HibernateException, GLException { checkPermission (GLPermission.POST, journal); session.buildLockRequest(LockOptions.UPGRADE).lock(journal); } /** * Lock an account in a given journal. * @param journal the journal. * @param acct the account. * @throws GLException if user doesn't have POST permission on this journal. * @throws HibernateException on database errors. */ public void lock (Journal journal, Account acct) throws HibernateException, GLException { checkPermission (GLPermission.POST, journal); AccountLock lck = getLock (journal, acct); } /** * Open underlying Hibernate session. * @throws HibernateException */ public synchronized Session open () throws HibernateException { return db.open(); } /** * Close underlying Hibernate session. * @throws HibernateException */ public synchronized void close () throws HibernateException { db.close(); } /** * @return underlying Hibernate Session. */ public Session session () { return db.session(); } /** * Begin hibernate transaction. * @return new Transaction */ public Transaction beginTransaction() throws HibernateException { return session.beginTransaction(); } /** * Begin hibernate transaction. * @param timeout timeout in seconds * @return new Transaction */ public Transaction beginTransaction(int timeout) throws HibernateException { Transaction tx = session.beginTransaction(); if (timeout > 0) tx.setTimeout (timeout); return tx; } public GLUser getUser (String nick) throws HibernateException { return (GLUser) session.createCriteria (GLUser.class) .add (Restrictions.eq ("nick", nick)) .uniqueResult(); } /** * set a journal's lockDate * @param journal the Journal * @param lockDate the lock date. * @throws HibernateException on database errors. * @throws GLException if users doesn't have global READ permission. */ public void setLockDate (Journal journal, Date lockDate) throws GLException, HibernateException { checkPermission (GLPermission.WRITE, journal); // Transaction tx = session.beginTransaction(); session.buildLockRequest(LockOptions.UPGRADE).lock(journal); journal.setLockDate (lockDate); // tx.commit(); } public void deleteBalanceCache (Journal journal, Account account, short[] layers) throws HibernateException { StringBuilder sb = new StringBuilder ("delete BalanceCache where journal = :journal"); if (account != null) sb.append (" and account = :account"); if (layers != null) sb.append (" and layers = :layers"); Query query = session.createQuery (sb.toString()) .setEntity ("journal", journal); if (account != null) query.setEntity ("account", account); if (layers != null) query.setString ("layers", layersToString (layers)); query.executeUpdate(); } public GLTransactionGroup createGroup (String name, List<GLTransaction> transactions) { GLTransactionGroup group = new GLTransactionGroup (name); Set txns = new HashSet(); for (GLTransaction t : transactions) txns.add (t); group.setTransactions(txns); session().save (group); return group; } public GLTransactionGroup findTransactionGroup (String name) { Criteria crit = session.createCriteria (GLTransactionGroup.class) .add (Restrictions.eq ("name", name)); crit.setMaxResults (1); return (GLTransactionGroup) crit.uniqueResult(); } public BigDecimal getBalance (Journal journal, Account acct, GLTransactionGroup group, short[] layers) throws HibernateException, GLException { checkPermission (GLPermission.READ, journal); BigDecimal balance = ZERO; for (GLTransaction transaction : (Set<GLTransaction>) group.getTransactions()) { if (transaction.getJournal().equals (journal)) { for (GLEntry entry : (List<GLEntry>) transaction.getEntries()) { if (acct.equals (entry.getAccount()) && entry.hasLayers(layers)) { if (entry.isIncrease ()) { balance = balance.add (entry.getAmount()); } else if (entry.isDecrease()) { balance = balance.subtract (entry.getAmount()); } } } } } return balance; } // ----------------------------------------------------------------------- // PUBLIC HELPERS // ----------------------------------------------------------------------- public short[] toLayers (String layers) { StringTokenizer st = new StringTokenizer (layers, ", "); short[] sa = new short[st.countTokens()]; for (int i=0; st.hasMoreTokens(); i++) sa[i] = Short.parseShort (st.nextToken()); return sa; } // ----------------------------------------------------------------------- // PRIVATE METHODS // ----------------------------------------------------------------------- private AccountLock getLock (Journal journal, Account acct) throws HibernateException { AccountLock key = new AccountLock (journal, acct); AccountLock lck = (AccountLock) session.get (AccountLock.class, key, LockOptions.UPGRADE); if (lck == null) session.buildLockRequest(LockOptions.UPGRADE).lock(journal); // need a journal level lock lck = (AccountLock) session.get (AccountLock.class, key, LockOptions.UPGRADE); // try again if (lck == null) { session.save (lck = key); session.flush(); } return lck; } private void createCheckpoint0 (Journal journal, Account acct, Date date, int threshold, short[] layers) throws HibernateException, GLException { if (acct.isCompositeAccount()) { Iterator iter = ((CompositeAccount) acct).getChildren().iterator(); while (iter.hasNext()) { Account a = (Account) iter.next(); createCheckpoint0 (journal, a, date, threshold, layers); } } else if (acct.isFinalAccount()) { Date sod = Util.floor (date); // sod = start of day invalidateCheckpoints (journal, new Account[] { acct }, sod, sod, layers); BigDecimal b[] = getBalances (journal, acct, sod, false, layers, 0L); if (b[1].intValue() >= threshold) { Checkpoint c = new Checkpoint (); c.setDate (sod); c.setBalance (b[0]); c.setJournal (journal); c.setAccount (acct); c.setLayers (layersToString(layers)); session.save (c); } } } private Account[] getAccounts (GLTransaction txn) { List list = txn.getEntries(); Account[] accounts = new Account[list.size()]; Iterator iter = list.iterator(); for (int i=0; iter.hasNext(); i++) { GLEntry entry = (GLEntry) iter.next(); accounts[i] = entry.getAccount(); } return accounts; } private List getAccountHierarchyIds (Account acct) throws GLException { if (acct == null) throw new GLException ("Invalid entry - account is null"); Account p = acct; List<Long> l = new ArrayList<Long>(); while (p != null) { l.add (p.getId()); p = p.getParent(); } return l; } private void invalidateCheckpoints (GLTransaction txn) throws HibernateException { Account[] accounts = getAccounts (txn); invalidateCheckpoints ( txn.getJournal(), accounts, txn.getPostDate(), null, null ); } private void invalidateCheckpoints (Journal journal, Account[] accounts, Date start, Date end, short[] layers) throws HibernateException { Criteria crit = session.createCriteria (Checkpoint.class) .add (Restrictions.eq ("journal", journal)); if (accounts.length > 0) crit = crit.add (Restrictions.in ("account", (Object[])accounts)); if (layers != null) crit.add (Restrictions.eq ("layers", layersToString(layers))); if (start.equals (end)) crit.add (Restrictions.eq ("date", start)); else { crit.add (Restrictions.ge ("date", start)); if (end != null) { crit.add (Restrictions.le ("date", end)); } } Iterator iter = crit.list().iterator(); while (iter.hasNext()) { Checkpoint cp = (Checkpoint) iter.next(); session.delete (cp); } } private BigDecimal applyEntries (BigDecimal balance, List entries) throws GLException { Iterator iter = entries.iterator(); while (iter.hasNext()) { GLEntry entry = (GLEntry) iter.next(); if (entry.isIncrease ()) { balance = balance.add (entry.getAmount()); } else if (entry.isDecrease()) { balance = balance.subtract (entry.getAmount()); } else { throw new GLException ( entry.toString() + " has invalid account type" ); } } return balance; } private Object getRuleImpl (String clazz) throws GLException { Object impl = ruleCache.get (clazz); if (impl == null) { synchronized (ruleCache) { impl = ruleCache.get (clazz); if (impl == null) { try { Class cls = Class.forName (clazz); impl = cls.newInstance(); ruleCache.put (clazz, impl); } catch (Exception e) { throw new GLException ("Invalid rule " + clazz, e); } } } } return impl; } private void addRules (Map<String,Object> ruleMap, Journal journal, List acctHierarchy, int offset) throws HibernateException, GLException { Query q = session.createQuery ( "from org.jpos.gl.RuleInfo where journal=:journal and account in (:accts) order by id" ); q.setParameter ("journal", journal); q.setParameterList ("accts", acctHierarchy, new LongType()); q.setCacheable (true); q.setCacheRegion ("rules"); Iterator iter = q.iterate(); while (iter.hasNext()) { RuleInfo ri = (RuleInfo) iter.next(); RuleEntry k = new RuleEntry (ri, ri.getAccount()); RuleEntry re = (RuleEntry) ruleMap.get (k.getKey()); if (re == null) ruleMap.put (k.getKey(), re = k); re.addOffset (offset); } } private void applyRules (GLTransaction txn, Collection rules) throws HibernateException, GLException { Iterator iter = rules.iterator(); while (iter.hasNext()) { RuleEntry re = (RuleEntry) iter.next(); RuleInfo ri = re.getRuleInfo(); JournalRule rule = (JournalRule) getRuleImpl (ri.getClazz()); rule.check ( this, txn, ri.getParam(), re.getAccount(), re.getOffsets(), ri.getLayerArray() ); } } private Collection getRules (GLTransaction txn) throws HibernateException, GLException { Map<String,Object> map = new LinkedHashMap<String,Object> (); Journal journal = txn.getJournal(); Query q = session.createQuery ( "from org.jpos.gl.RuleInfo where journal=:journal and account is null order by id" ); q.setParameter ("journal", journal); Iterator iter = q.list().iterator(); while (iter.hasNext()) { RuleInfo ri = (RuleInfo) iter.next(); RuleEntry re = new RuleEntry (ri); map.put (re.getKey(), re); } iter = txn.getEntries().iterator(); for (int i=0; iter.hasNext(); i++) { GLEntry entry = (GLEntry) iter.next(); addRules (map, journal, getAccountHierarchyIds (entry.getAccount()), i); } return map.values(); } private BigDecimal[] getChartBalances (Journal journal, CompositeAccount acct, Date date, boolean inclusive, short[] layers, long maxId) throws HibernateException, GLException { BigDecimal balance[] = { ZERO, ZERO }; Iterator iter = ((CompositeAccount) acct).getChildren().iterator(); while (iter.hasNext()) { Account a = (Account) iter.next(); BigDecimal[] b = getBalances (journal, a, date, inclusive, layers, maxId); if (a.isDebit()) { balance[0] = balance[0].add (b[0]); balance[1] = balance[1].add (b[1]); } else if (a.isCredit()) { balance[0] = balance[0].subtract (b[0]); balance[1] = balance[1].subtract (b[1]); } else { throw new GLException ("Account " + a + " has wrong type"); } // session.evict (a); FIXME this conflicts with r251 (cascade=evict genearting a failed to lazily initialize a collection } return balance; } private Iterator findSummarizedGLEntries (Journal journal, Date start, Date end, boolean credit, short layer) throws HibernateException, GLException { StringBuffer qs = new StringBuffer ( "select entry.account, sum(entry.amount)" + " from org.jpos.gl.GLEntry entry," + " org.jpos.gl.GLTransaction txn" + " where txn.id = entry.transaction" + " and credit = :credit" + " and txn.journal = :journal" + " and entry.layer = :layer" ); boolean equalDate = start.equals (end); if (equalDate) { qs.append (" and txn.postDate = :date"); } else { qs.append (" and txn.postDate >= :start"); qs.append (" and txn.postDate <= :end"); } qs.append (" group by entry.account"); Query q = session.createQuery (qs.toString()); q.setLong ("journal", journal.getId()); q.setParameter ("credit", credit ? "Y" : "N"); q.setShort ("layer", layer); if (equalDate) q.setParameter ("date", start); else { q.setParameter ("start", start); q.setParameter ("end", end); } return q.iterate(); } private void deleteGLTransactions (Journal journal, Date start, Date end) throws HibernateException, GLException { boolean equalDate = start.equals (end); StringBuffer qs = new StringBuffer ( "from org.jpos.gl.GLTransaction where journal = :journal" ); if (equalDate) { qs.append (" and postDate = :date"); } else { qs.append (" and postDate >= :start"); qs.append (" and postDate <= :endDate"); } Query q = session.createQuery (qs.toString()); q.setLong ("journal", journal.getId()); if (equalDate) q.setParameter ("date", start); else { q.setParameter ("start", start); q.setParameter ("endDate", end); } ScrollableResults sr = q.scroll(ScrollMode.FORWARD_ONLY); while (sr.next()) { session.delete (sr.get(0)); } } private static Short[] toShortArray (short[] i) { if (i == null) return new Short[0]; Short[] sa = new Short[i.length]; for (int j=0; j<i.length; j++) sa[j] = new Short(i[j]); return sa; } private String layersToString (short[] layers) { StringBuffer sb = new StringBuffer(); Arrays.sort (layers); for (int i=0; i<layers.length; i++) { if (i>0) sb.append ('.'); sb.append (Short.toString(layers[i])); } return sb.toString(); } private long getMaxGLEntryId () { Criteria crit = session.createCriteria (GLEntry.class); crit.addOrder (Order.desc ("id")); crit.setMaxResults (1); GLEntry entry = (GLEntry) crit.uniqueResult(); return entry != null ? entry.getId() : 0L; } private long getSafeMaxGLEntryId() { return Math.max (getMaxGLEntryId()-SAFE_WINDOW, 0L); } public void overrideSafeWindow (long l) { this.SAFE_WINDOW = l; } private void recurseChildren (Account acct, List<Long> list) { for (Account a : acct.getChildren()) { if (a.isFinalAccount()) list.add (a.getId()); else recurseChildren (a, list); } } private List<Long> getChildren (Account acct) { List<Long> list = new ArrayList<Long>(); recurseChildren (acct, list); return list; } public String toString() { return super.toString() + "[DB=" + db.toString() + "]"; } /* private void dumpRules (Collection rules) { log.warn ("--- rules ---"); Iterator iter = rules.iterator(); while (iter.hasNext()) { log.warn (iter.next()); } } */ }