org.jpos.gl.GLSession.java Source code

Java tutorial

Introduction

Here is the source code for org.jpos.gl.GLSession.java

Source

/*
 * MiniGL
 * Copyright (C) 2005 Alejandro Revilla
 *
 * 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.jpos.gl;

import java.util.Map;
import java.util.Set;
import java.util.Date;
import java.util.Calendar;
import java.util.List;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.StringTokenizer;
import java.math.BigDecimal;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Criteria;
import org.hibernate.LockMode;
import org.hibernate.ScrollMode;
import org.hibernate.Transaction;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.ScrollableResults;
import org.hibernate.HibernateException;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Expression;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.type.LongType;
import org.hibernate.Hibernate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jpos.ee.DB;

/**
 * MiniGL facility entry point.
 *
 * @author <a href="mailto:apr@jpos.org">Alejandro Revilla</a>
 */
public class GLSession {
    private static Configuration cfg;
    private static SessionFactory sf;
    private static Map ruleCache;
    private GLUser user;
    private Session session;
    private static final Log log = LogFactory.getLog(GLSession.class);
    private long checkpoint;
    private DB db;
    public static final short[] LAYER_ZERO = new short[] { 0 };
    /** 
     * "0.00"
     */
    public static final BigDecimal ZERO = new BigDecimal("0.00");
    /** 
     * "0" (no decimals)
     */
    public static final BigDecimal Z = new BigDecimal("0");

    static {
        try {
            // init ();
            ruleCache = new HashMap();
        } catch (Exception e) {
            log.fatal(e);
        }
    }

    /**
     * Construct a GLSession for a given user.
     *
     * User has to exist in MiniGL gluser table.
     * @see GLUser
     *
     * @param username the user name.
     */
    public GLSession(String username) throws HibernateException, GLException {
        super();
        this.db = new DB();
        session = db.open();
        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 User
     */
    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.
     * @see GLUser
     *
     * @param db EE DB
     * @param username the user name.
     */
    public GLSession(DB db, String username) throws HibernateException, GLException {
        super();
        this.db = db;
        session = db.open();
        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.
     * @param db EE DB
     * @see User
     */
    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);
    }

    /**
     * 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) {
        Iterator iter = user.getPermissions().iterator();
        while (iter.hasNext()) {
            GLPermission p = (GLPermission) iter.next();
            Journal pj = p.getJournal();
            if (action.equals(p.getName()) && (pj == null || (pj.getId() == j.getId()))) {
                return true;
            }
        }
        return false;
    }

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

    /**
     * @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
     * @throws GLException if user doesn't have permissions, or type mismatch
     */
    public void addAccount(CompositeAccount parent, Account acct) 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());
        session.save(acct);
        // acct.setParent (parent);
        parent.getChildren().add(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.
     * @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 {
        checkPermission(GLPermission.READ);
        Query q = session.createQuery("from journal in class org.jpos.gl.Journal where name=:name");
        q.setParameter("name", name);
        Iterator iter = q.list().iterator();
        return (Journal) (iter.hasNext() ? iter.next() : null);
    }

    /**
     * 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());
        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 jounral.
     * @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 jounral.
     */
    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() != 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 destinationJournal where to move former transactions. A null value
    would delete former transactions.
     * @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 jounral.
     */
    public List findTransactions(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(Expression.eq("journal", journal));

        if (start != null && start.equals(end))
            crit.add(Expression.eq(dateField, start));
        else {
            if (start != null)
                crit.add(Expression.ge(dateField, start));
            if (end != null)
                crit.add(Expression.le(dateField, end));
        }
        if (searchString != null)
            crit.add(Expression.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 destinationJournal where to move former transactions. A null value
    would delete former transactions.
     * @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 jounral.
     */
    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);
    }

    /**
     * @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 layers the layers.
     * @return current balance.
     * @throws GLException if user doesn't have READ permission on this jounral.
     */
    public BigDecimal getBalance(Journal journal, Account acct, short layer)
            throws HibernateException, GLException {
        return getBalances(journal, acct, null, true, new short[] { layer })[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 jounral.
     */
    public BigDecimal getBalance(Journal journal, Account acct, short[] layers)
            throws HibernateException, GLException {
        return getBalances(journal, acct, null, true, layers)[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 jounral.
     */
    public BigDecimal getBalance(Journal journal, Account acct, String layers)
            throws HibernateException, GLException {
        return getBalances(journal, acct, null, true, toLayers(layers))[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 jounral.
     */
    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 layers layers
     * @return balance at given date.
     * @throws GLException if user doesn't have READ permission on this jounral.
     */
    public BigDecimal getBalance(Journal journal, Account acct, Date date, short layer)
            throws HibernateException, GLException {
        return getBalances(journal, acct, date, true, new short[] { layer })[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 jounral.
     */
    public BigDecimal getBalance(Journal journal, Account acct, Date date, short[] layers)
            throws HibernateException, GLException {
        return getBalances(journal, acct, date, true, layers)[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 jounral.
     */
    public BigDecimal[] getBalances(Journal journal, Account acct, Date date, boolean inclusive)
            throws HibernateException, GLException {
        return getBalances(journal, acct, date, inclusive, LAYER_ZERO);
    }

    /**
     * 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 
     * @return array of 2 BigDecimals with balance and entry count.
     * @throws GLException if user doesn't have READ permission on this jounral.
     */
    public BigDecimal[] getBalances(Journal journal, Account acct, Date date, boolean inclusive, short[] layers)
            throws HibernateException, GLException {
        checkPermission(GLPermission.READ, journal);
        BigDecimal balance[] = { ZERO, Z };
        if (acct.getChildren() != null) {
            if (acct.isChart()) {
                return getChartBalances(journal, (CompositeAccount) acct, date, inclusive, layers);
            }
            Iterator iter = acct.getChildren().iterator();
            while (iter.hasNext()) {
                Account a = (Account) iter.next();
                BigDecimal[] b = getBalances(journal, a, date, inclusive, layers);
                balance[0] = balance[0].add(b[0]);
                session.evict(a);
            }
        } else if (acct instanceof FinalAccount) {
            Criteria crit = session.createCriteria(GLEntry.class).add(Expression.eq("account", acct))
                    .add(Expression.in("layer", toShortArray(layers)));
            crit = crit.createCriteria("transaction").add(Expression.eq("journal", journal));
            if (date != null) {
                if (inclusive) {
                    crit.add(Expression.lt("postDate", Util.tomorrow(date)));
                } else {
                    date = Util.floor(date);
                    crit.add(Expression.lt("postDate", date));
                }
            }
            Checkpoint chkp = getRecentCheckpoint(journal, acct, date, inclusive, layers);
            if (chkp != null) {
                balance[0] = chkp.getBalance();
                crit.add(Expression.gt("postDate", chkp.getDate()));
            }
            List l = crit.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 jounral.
     */
    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).add(Expression.eq("account", acct))
                .add(Expression.in("layer", toShortArray(layers)));
        crit = crit.createCriteria("transaction").add(Expression.eq("journal", journal))
                .add(Expression.ge("postDate", start)).add(Expression.le("postDate", end));

        BigDecimal initialBalance[] = getBalances(journal, acct, start, false, layers);
        List entries = crit.list();
        BigDecimal finalBalance = applyEntries(initialBalance[0], entries);

        return new AccountDetail(journal, acct, initialBalance[0], finalBalance, start, end, 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 jounral.
     */
    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(Expression.eq("journal", journal))
                .add(Expression.eq("account", acct));

        if (layers != null)
            crit.add(Expression.eq("layers", layersToString(layers)));

        if (date != null) {
            if (inclusive)
                crit.add(Expression.le("date", date));
            else
                crit.add(Expression.lt("date", date));
        }
        crit.addOrder(Order.desc("date"));
        crit.setMaxResults(1);
        return (Checkpoint) 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 jounral.
     */
    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 jounral.
     */
    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.lock(journal, LockMode.UPGRADE);
        createCheckpoint0(journal, acct, date, threshold, layers);
        // tx.commit();
    }

    /**
     * Lock a journal.
     * @param journal the journal.
     * @throws HibernateException on database errors.
     * @throws GLException if user doesn't have POST permission on this jounral.
     */
    public void lock(Journal journal) throws HibernateException, GLException {
        checkPermission(GLPermission.POST, journal);
        session.lock(journal, LockMode.UPGRADE);
    }

    /**
     * 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 jounral.
     * @throws HibernateException on database errors.
     */
    public void lock(Journal journal, Account acct) throws HibernateException, GLException {
        checkPermission(GLPermission.POST, journal);
        AccountLock lck = getLock(journal, acct, false);
        if (lck == null) {
            // Transaction tx = session.beginTransaction();
            session.lock(journal, LockMode.UPGRADE);
            lck = getLock(journal, acct, true);
            // tx.commit();
            lck = getLock(journal, acct, false); // re-get it
        }
    }

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

    /**
     * @return Hibernate's session factory
     */
    public SessionFactory getSessionFactory() {
        return sf;
    }

    /**
     * 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(Expression.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.lock(journal, LockMode.UPGRADE);
        journal.setLockDate(lockDate);
        // tx.commit();
    }

    // -----------------------------------------------------------------------
    // 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, boolean create) throws HibernateException {
        AccountLock lck = new AccountLock(journal, acct);
        try {
            lck = (AccountLock) session.load(AccountLock.class, lck, LockMode.UPGRADE);
        } catch (ObjectNotFoundException e) {
            if (create)
                session.save(lck);
            else
                lck = null;
        }
        return lck;
    }

    private void createCheckpoint0(Journal journal, Account acct, Date date, int threshold, short[] layers)
            throws HibernateException, GLException {
        if (acct instanceof CompositeAccount) {
            Iterator iter = ((CompositeAccount) acct).getChildren().iterator();
            while (iter.hasNext()) {
                Account a = (Account) iter.next();
                createCheckpoint0(journal, a, date, threshold, layers);
            }
        } else if (acct instanceof FinalAccount) {
            Date sod = Util.floor(date); // sod = start of day
            invalidateCheckpoints(journal, new Account[] { acct }, sod, sod, layers);
            BigDecimal b[] = getBalances(journal, acct, date, true, layers);
            if (b[1].intValue() >= threshold) {
                Checkpoint c = new Checkpoint();
                c.setDate(date);
                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 l = new ArrayList();
        for (int i = 0; p != null; i++) {
            l.add(new Long(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(Expression.eq("journal", journal))
                .add(Expression.in("account", accounts));

        if (layers != null)
            crit.add(Expression.eq("layers", layersToString(layers)));
        if (start.equals(end))
            crit.add(Expression.eq("date", start));
        else {
            crit.add(Expression.ge("date", start));
            if (end != null) {
                crit.add(Expression.le("date", end));
            }
        }
        Iterator iter = crit.list().iterator();
        while (iter.hasNext()) {
            Checkpoint cp = (Checkpoint) iter.next();
            session.delete(cp);
        }
        session.flush();
    }

    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 static void init() throws Exception {
        synchronized (GLSession.class) {
            if (cfg != null)
                return;

            cfg = new Configuration();
            cfg.configure();
            sf = cfg.buildSessionFactory();
        }
    }

    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 dumpRules(Collection rules) {
        log.warn("--- rules ---");
        Iterator iter = rules.iterator();
        while (iter.hasNext()) {
            log.warn(iter.next());
        }
    }

    private void addRules(Map 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 map = new LinkedHashMap();
        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) 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);
            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);
        }
        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 void reset() {
        checkpoint = System.currentTimeMillis();
    }

    private long checkPoint() {
        long now = System.currentTimeMillis();
        long diff = now - checkpoint;
        checkpoint = now;
        return diff;
    }

    private void checkPoint(String s) {
        System.out.println(s + " [" + checkPoint() + "ms]");
    }

    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();
    }
}