/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package com.queplix.core.modules.eql.ejb;

import com.queplix.core.error.ErrorHelper;
import com.queplix.core.error.GenericSystemException;
import com.queplix.core.integrator.security.LogonSession;
import com.queplix.core.jxb.entity.Afterdelete;
import com.queplix.core.jxb.entity.Afternew;
import com.queplix.core.jxb.entity.Afterupdate;
import com.queplix.core.jxb.entity.Beforedelete;
import com.queplix.core.jxb.entity.Beforeupdate;
import com.queplix.core.jxb.entity.Dataset;
import com.queplix.core.jxb.entity.Delete;
import com.queplix.core.jxb.entity.Efield;
import com.queplix.core.jxb.entity.Entity;
import com.queplix.core.jxb.entity.Fkeys;
import com.queplix.core.jxb.entity.Update;
import com.queplix.core.modules.config.utils.EntityHelper;
import com.queplix.core.modules.eql.CompoundKey;
import com.queplix.core.modules.eql.EQLDReq;
import com.queplix.core.modules.eql.EQLDRes;
import com.queplix.core.modules.eql.EQLEReq;
import com.queplix.core.modules.eql.EQLERes;
import com.queplix.core.modules.eql.EQLFactory;
import com.queplix.core.modules.eql.EQLNullObject;
import com.queplix.core.modules.eql.EQLObject;
import com.queplix.core.modules.eql.EQLReq;
import com.queplix.core.modules.eql.EQLReqDataset;
import com.queplix.core.modules.eql.EQLReqField;
import com.queplix.core.modules.eql.EQLReqMetaData;
import com.queplix.core.modules.eql.EQLRes;
import com.queplix.core.modules.eql.EQLResCell;
import com.queplix.core.modules.eql.EQLResRecord;
import com.queplix.core.modules.eql.EQLSession;
import com.queplix.core.modules.eql.error.EQLConstraintViolationException;
import com.queplix.core.modules.eql.error.EQLDeadlockException;
import com.queplix.core.modules.eql.error.EQLDeleteConflictException;
import com.queplix.core.modules.eql.error.EQLDuplicateKeyException;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.modules.eql.error.EQLIndexConflictException;
import com.queplix.core.modules.eql.error.EQLRemoveException;
import com.queplix.core.modules.eql.error.EQLSystemException;
import com.queplix.core.modules.eql.parser.EQLAgent;
import com.queplix.core.modules.eql.parser.EQLAgentFactory;
import com.queplix.core.modules.eql.parser.EQLIntPreparedStatement;
import com.queplix.core.modules.eql.parser.EQLInterpreter;
import com.queplix.core.modules.eql.update.EntityUpdate;
import com.queplix.core.modules.eql.update.EntityUpdateFactory;
import com.queplix.core.modules.eql.update.EntityUpdateObject;
import com.queplix.core.modules.eql.utils.cache.EQLResCacheManager;
import com.queplix.core.modules.eql.utils.cache.EQLResCacheObject;
import com.queplix.core.utils.JNDINames;
import com.queplix.core.utils.async.ASyncObject;
import com.queplix.core.utils.async.ASyncRequest;
import com.queplix.core.utils.async.JMSClient;
import com.queplix.core.utils.sql.error.SQLDeadlockException;
import com.queplix.core.utils.sql.error.SQLDeleteConflictException;
import com.queplix.core.utils.sql.error.SQLDuplicateKeyException;
import com.queplix.core.utils.sql.error.SQLIndexConflictException;

import java.util.HashMap;

/**
 * EQL Manager session EJB.
 *
 * @author Baranov Andrey [ALB]
 * @author [ONZ] Oleg N. Zhovtanyuk
 * @version $Revision: 1.1.1.1 $ $Date: 2005/09/12 15:30:23 $
 */
public class EQLManagerEJB
        extends AbstractEQLSupportedEJB {

    // =============================================================== Constants

    /**
     * Updates status: Continue updates process
     */
    private static final int CONTINUE = 0;

    /**
     * Updates status: Skip updates process
     */
    private static final int SKIP = 1;

    private int cntAffectedRows = 0;
    // ================================================================== Fields

    private AsyncClient asyncClient;

    // ========================================================== Public methods

    /**
     * Initializes bean.
     */
    public void ejbCreate() {
        INFO("EQL Manager EJB created - " + hashCode());
        asyncClient = new AsyncClient();
    }

    // --------------
    // SELECT METHODS
    // --------------

    /**
     * EQL <code>SELECT</code> query - selects a result set.
     *
     * @param ls     user logon session
     * @param entity selected Entity object
     * @return EQLERes response object
     * @throws EQLException
     */
    public EQLERes select(LogonSession ls, Entity entity)
            throws EQLException {
        return select(ls, entity, null);
    }

    /**
     * EQL <code>SELECT</code> query - selects a result set.
     *
     * @param ls     user logon session
     * @param entity selected Entity object
     * @param meta   EQl meta data
     * @return EQLERes response object
     * @throws EQLException
     */
    public EQLERes select(LogonSession ls, Entity entity, EQLReqMetaData meta)
            throws EQLException {
        return select(ls, entity, null, null, meta);
    }

    /**
     * EQL <code>SELECT</code> query - selects a result set.
     *
     * @param ls            user logon session
     * @param entity        selected Entity object
     * @param pkeyEntity    primary keys entity
     * @param compoundPkeys compound keys
     * @param meta          EQL meta data
     * @return EQLERes response object
     * @throws EQLException
     */
    public EQLERes select(LogonSession ls,
                          Entity entity,
                          Entity pkeyEntity,
                          CompoundKey[] compoundPkeys,
                          EQLReqMetaData meta)
            throws EQLException {

        // Init.
        EQLRes res = null;
        EQLIntPreparedStatement ps = null;
        int psCount = 1;
        int compounds = (compoundPkeys == null) ? 0:compoundPkeys.length;

        if(compounds == 0) {
            // Try find in cache.
            res = EQLResCacheManager.getInstance().findEQLResInCache(entity);
        }

        if(res == null) {
            // Build EQL request.
            String eqlQuery = "SELECT " + entity.getName() + ".*";

            if(compounds > 0) {
                // Add constraints.
                ps = new EQLIntPreparedStatement();
                eqlQuery += " WHERE ";

                for(int i = 0; i < compounds; i++) {

                    CompoundKey compoundPkey = compoundPkeys[i];
                    int size = compoundPkey.size();

                    if(i > 0) {
                        eqlQuery += " OR ";
                    }
                    eqlQuery += "(";

                    int key_num = 0;
                    for(int j = 0; j < pkeyEntity.getEfieldCount(); j++) {
                        Efield field = pkeyEntity.getEfield(j);
                        if(!field.getPkey().booleanValue()) {
                            continue;
                        }

                        // Pimary key found...
                        if(key_num >= size) {
                            break;
                        }

                        // .. add pkey in request
                        if(key_num > 0) {
                            eqlQuery += " AND ";
                        }
                        eqlQuery += field.getId() + " = ?";
                        ps.setObject(psCount++, EQLObject.getInstance(
                                compoundPkey.getKey(key_num)));

                        key_num++;
                    }

                    eqlQuery += ")";
                }
            }

            // Do select.
            res = select(ls, eqlQuery, ps, meta);
        }

        return (EQLERes) res;
    }

    /**
     * EQL <code>SELECT</code> query - selects a result set.
     *
     * @param ls       user logon session
     * @param eqlQuery EQL query
     * @return EQLRes response object
     * @throws EQLException
     */
    public EQLRes select(LogonSession ls, String eqlQuery)
            throws EQLException {
        return select(ls, eqlQuery, null);
    }

    /**
     * EQL <code>SELECT</code> query - selects a result set.
     *
     * @param ls       user logon session
     * @param eqlQuery EQL query
     * @param eqlPS    EQLIntPreparedStatement object
     * @return EQLRes response object
     * @throws EQLException
     */
    public EQLRes select(LogonSession ls,
                         String eqlQuery,
                         EQLIntPreparedStatement eqlPS)
            throws EQLException {
        return select(ls, eqlQuery, eqlPS, null);
    }

    /**
     * EQL <code>SELECT</code> query - selects a result set.
     *
     * @param ls       user logon session
     * @param eqlQuery EQL query
     * @param eqlPS    EQLIntPreparedStatement object
     * @param meta     EQL meta data
     * @return EQLRes response object
     * @throws EQLException
     */
    public EQLRes select(LogonSession ls,
                         String eqlQuery,
                         EQLIntPreparedStatement eqlPS,
                         EQLReqMetaData meta)
            throws EQLException {

        // Initialization
        long time = System.currentTimeMillis();
        EQLSession eqlSession = getEQLSession(ls);
        EQLRes res = null;

        try {
            res = doSelect(eqlSession, eqlQuery, eqlPS, meta);

        } catch (EQLException ex) {

            // Make explicit rollback on checked exception.
            setRollbackOnly();
            throw ex;

        } catch (Throwable t) {
            ErrorHelper.throwSystemException(t, this);
        }

        // Ok.
//        INFO( "EQL SELECT completed. Time (ms) = " + ( System.currentTimeMillis() - time ) );
        return res;

    }

    // ------------------
    // SELECT NEW METHODS
    // ------------------

    /**
     * EQL <code>SELECT NEW</code> query - selects a new entity field value.
     *
     * @param ls    user logon session
     * @param field selected Efield object
     * @return EQLRes response object
     * @throws EQLException
     */
    public EQLRes selectNew(LogonSession ls, Efield field)
            throws EQLException {
        return __selectNew(getEQLSession(ls), field);
    }

    /**
     * EQL <code>SELECT NEW</code> query - selects a new entity fields values.
     *
     * @param ls     user logon session
     * @param entity selected Entity object
     * @return EQLERes response object
     * @throws EQLException
     */
    public EQLERes selectNew(LogonSession ls, Entity entity)
            throws EQLException {

        // Initializarion.
        long time = System.currentTimeMillis();
        EQLSession eqlSession = getEQLSession(ls);
        EQLERes ret = new EQLERes(entity);

        try {
            // Select new records.
            __selectNew(eqlSession, entity, ret);

        } catch (EQLException ex) {

            // Make explicit rollback on checked exception.
            setRollbackOnly();
            throw ex;

        } catch (Throwable t) {
            ErrorHelper.throwSystemException(t, this);
        }

        // Ok.
//        INFO( "EQL SELECT NEW completed. Time (ms) = " + ( System.currentTimeMillis() - time ) );
        return ret;

    }

    // -------------------
    // SELECT COUNT METHOD
    // -------------------

    /**
     * EQL <code>COUNT</code> query - counts records in database for
     * the given EQL response <code>res</code>.
     *
     * @param ls  user logon session
     * @param res EQLERes object
     * @return count records in database
     * @throws EQLException
     */
    public int selectCount(LogonSession ls, EQLRes res)
            throws EQLException {
        return doCount(getEQLSession(ls), res);
    }

    // --------------
    // UPDATE METHODS
    // --------------

    /**
     * EQL <code>UPDATE</code> query - updates / inserts / deletes
     * database record(s).
     *
     * @param ls  user logon session
     * @param res EQLERes object
     * @throws EQLException
     */
    public void update(LogonSession ls, EQLERes res)
            throws EQLException {

        update(ls, res, null);

    }

    /**
     * EQL <code>UPDATE</code> query - updates / inserts / deletes
     * database record(s).
     *
     * @param ls     user logon session
     * @param res    EQLERes object
     * @param record EQLResRecord object
     * @throws EQLException
     */
    public void update(LogonSession ls, EQLERes res, EQLResRecord record)
            throws EQLException {

        // Initialization
        long time = System.currentTimeMillis();
        EQLSession eqlSession = getEQLSession(ls);

        try {
            __update(eqlSession, res, record);

        } catch (EQLException ex) {

            // Make explicit rollback on checked exception.
            setRollbackOnly();
            throw ex;

        } catch (EQLConstraintViolationException e) {
            throw e;
        } catch (Throwable t) {
            ErrorHelper.throwSystemException(t, this);
        }

        // Ok.
        INFO("EQL UPDATE completed. Time (ms) = " + (System.currentTimeMillis()
                - time));

    }

    /**
     * EQL <code>DELETE</code> function.
     *
     * @param ls            user logon session
     * @param entity        base entity
     * @param compoundPkeys compound primary keys array
     * @throws EQLException
     */
    public void delete(LogonSession ls, Entity entity,
                       CompoundKey[] compoundPkeys)
            throws EQLException {

        // Check arguments.
        if(entity == null || compoundPkeys == null) {
            throw new IllegalStateException();
        }

        EQLERes res = new EQLERes(entity);

        // Add records.
        int fields = entity.getEfieldCount();
        for(int i = 0; i < compoundPkeys.length; i++) {

            CompoundKey compoundPkey = compoundPkeys[i];
            int size = compoundPkey.size();

            // Create EQLResRecord object.
            EQLResRecord resRecord = new EQLResRecord(false, fields);
            res.addRecord(resRecord);

            int key_num = 0;
            for(int j = 0; j < fields; j++) {

                Efield field = entity.getEfield(j);
                if(!field.getPkey().booleanValue()) {
                    continue;
                }

                // Pimary key found...
                if(key_num >= size) {
                    break;
                }

                // Create EQLResCell object for concrete field.
                EQLReqField reqField = new EQLReqField(entity, field);
                EQLObject o = EQLObject.getInstance(compoundPkey.getKey(
                        key_num));
                EQLResCell resCell = new EQLResCell(reqField, o);
                resRecord.addData(resCell, key_num);

                key_num++;
            }
        }

        // Do delete.
        res.markAllAsDelete();
        update(ls, res);

    }

    // ---------------
    // SERVICE METHODS
    // ---------------

    /**
     * Access method to cache manager.
     *
     * @param ls     logon session
     * @param entity Entity object
     * @return EQLResCacheObject object
     * @throws EQLException
     */
    public EQLResCacheObject getCache(LogonSession ls, Entity entity)
            throws EQLException {
        EQLResCacheObject cache = EQLResCacheManager.getInstance()
                .getCacheObject(entity);
        if(cache == null) {
            select(ls, entity);
            cache = EQLResCacheManager.getInstance().getCacheObject(entity);
            if(cache == null) {
                throw new EQLSystemException(
                        "Entity '" + entity.getName() + "' can't be cached.");
            }
        }
        return cache;
    }

    public int getAffectedRowsCounter() {
        return cntAffectedRows;
    }
    // ========================================================= Helper methods

    //
    // Call EQL SELECT NEW query.
    //

    void __selectNew(EQLSession eqlSession, Entity entity, EQLERes res)
            throws EQLException {

        // Create new record.
        int size = entity.getEfieldCount();
        EQLResRecord record = new EQLResRecord(true, size);
        res.addRecord(record);

        // Initialize fields.
        for(int i = 0; i < size; i++) {
            Efield field = entity.getEfield(i);
            EQLReqField reqField = new EQLReqField(entity, field);

            // .. check 'eql-defsrc' attribute to boost perfomance
            EQLRes __res = null;
            if(field.getEqlDefsrc() != null) {
                __res = __selectNew(eqlSession, field);
            }

            EQLResCell cell;
            if(__res != null && __res.size() > 0) {
                // .. found default data
                cell = __res.getRecord(0).getResCell(0);

            } else {
                // .. default data is empty
                cell = new EQLResCell(reqField, EQLNullObject.getInstance());
            }

            // .. add data cell
            record.addData(cell, i);
        }

        // Initialize datasets.
        size = entity.getDatasetCount();
        for(int i = 0; i < size; i++) {
            Dataset ds = entity.getDataset(i);
            Entity dsEntity = eqlSession.findEntity(ds.getEntity());
            EQLReqDataset reqDataset = new EQLReqDataset(entity, ds, dsEntity);
            EQLDRes dsRes = new EQLDRes(reqDataset);

            // check required
            // and select new record for dataset
            if(ds.getRequired().booleanValue()) {
                __selectNew(eqlSession, dsEntity, dsRes);
            }

            record.addDRes(dsRes);
        }

        // Run post-new scripts.
        boolean hasScripts = (entity.getScripts() != null);
        if(hasScripts) {
            Afternew[] afterNews = entity.getScripts().getAfternew();
            if(afterNews != null) {
                for(int j = 0; j < afterNews.length; j++) {
                    doEntityUpdate(eqlSession, res, record, afterNews[j]);
                }
            }
        }

    }

    //
    // Call EQL SELECT NEW query.
    //
    EQLRes __selectNew(EQLSession eqlSession, Efield field)
            throws EQLException {
        String eqlQuery = "NEW " + field.getId();
        return doSelect(eqlSession, eqlQuery, null, null);
    }

    //
    // Call EQL UPDATE query (update / insert / delete).
    //
    void __update(EQLSession eqlSession, EQLERes res, EQLResRecord record)
            throws EQLException {

        // Unlock records first.
        getLockManagerLocal().unlock(res, eqlSession.getLogonSession(), record);

        // Get array of records, because records can be removed from Res object.
        int records = 0;
        EQLResRecord[] recordsArray;
        if(record == null) {
            records = res.size();
            recordsArray = (EQLResRecord[]) res.getRecords().toArray(
                    new EQLResRecord[0]);
        } else {
            records = 1;
            recordsArray = new EQLResRecord[]{record};
        }

        // Main updates cycle.
        for(int i = 0; i < records; i++) {

            EQLResRecord resRecord = recordsArray[i];
            if(getLogger().isDebugEnabled()) {
                DEBUG("Updating record #" + i
                        + "\n\t base entity: " + res.getEntity().getName()
                        + "\n\t delete? - " + resRecord.doDelete()
                        + "\n\t new? - " + resRecord.isNew()
                        + "\n\t changed? - " + resRecord.isChanged());
            }

            int status;
            if(resRecord.doDelete()) {
                status = doDelete(eqlSession, res, resRecord);
            } else {
                status = doUpdate(eqlSession, res, resRecord);
            }

            if(status == SKIP) {
                break;
            }
        }
    }

    // ========================================================= Agent's methods

    //
    // Does EQL COUNT query.
    //

    public int doCount(EQLSession eqlSession, EQLRes res)
            throws EQLException {

        EQLAgent agent = EQLAgentFactory.getInstance().getEQLAgent(res);
        return agent.doCount(eqlSession, res);
    }

    //
    // Does EQL SELECT query.
    //
    private EQLRes doSelect(EQLSession eqlSession,
                            String eqlQuery,
                            EQLIntPreparedStatement eqlPS,
                            EQLReqMetaData meta)
            throws EQLException {

        EQLRes res = null;

        if(getLogger().isInfoEnabled()) {
            INFO("...EQL query: " + eqlQuery);
//            INFO( "...EQL meta: " + meta );
        }

        // 1. Parse EQL query and build EQLReq
        EQLInterpreter interpreter = EQLFactory.getInstance().getEqlInterpreter(
                eqlSession);
        interpreter.setPreparedStatement(eqlPS);
        interpreter.setEQLReqMetaData(meta);
        EQLReq req = interpreter.build(eqlQuery);

//        INFO( "...build (parse) EQL request completed. Class: " + req.getClass() );

        // 2. Detect can we take response from cache
        boolean needToCache = false;
        Entity baseEntity = (req instanceof EQLEReq) ? ((EQLEReq) req)
                .getEntity():null;
        if(baseEntity != null && baseEntity.getCache().booleanValue()) {
            needToCache = true;
        }

//        INFO( "...detect 'need to store in cache' flag: " + needToCache );

        if(needToCache) {
            // 3. Try to find response in cache
            res = EQLResCacheManager.getInstance().findEQLResInCache(
                    baseEntity);
        }

        if(res != null) {
            DEBUG("..response found in the cache!");
        }

        if(res == null) {

            if((req instanceof EQLEReq) && (meta == null || !meta
                    .isIgnoreAllDatasets())) {
                // 4. Add datasets in EQL request
                for(int i = 0; i < baseEntity.getDatasetCount(); i++) {
                    Dataset dataset = baseEntity.getDataset(i);
                    if(meta != null && meta.isLazyLoadDataset(dataset)) {
                        // .. ignore this dataset
                        continue;
                    }

                    INFO("...add dataset '" + dataset.getName()
                            + "' in EQL request");

                    Entity datasetEntity = eqlSession.findEntity(
                            dataset.getEntity());
                    EQLReqDataset reqDataset = new EQLReqDataset(baseEntity,
                            dataset, datasetEntity);
                    EQLDReq dReq = new EQLDReq(reqDataset);
                    EQLReq __req = EQLFactory.getInstance().getEqlInterpreter(
                            eqlSession).build(dataset.getEql());
                    dReq.copyOf(__req);

                    // add EQLDReq in request
                    req.addDReq(dReq);
                }
            }

            // 5. Call EQLAgent
            EQLAgent agent = EQLAgentFactory.getInstance().getEQLAgent(req);
            res = agent.doSelect(eqlSession, req);
        }

        if(needToCache) {
            // 6.  Put respone to cache.
            EQLResCacheManager.getInstance().saveEQLResInCache((EQLERes) res);
        }

        // 7. Ok
        return res;
    }

    //
    // Does the record insert/update.
    //
    private int doUpdate(EQLSession eqlSession, EQLERes res,
                         EQLResRecord resRecord)
            throws EQLException {

        // Get base entity.
        Entity entity = res.getEntity();

        // Is record changed?
        boolean isChanged = resRecord.isChanged();
        if(!isChanged) {
            return CONTINUE;
        }

        try {

            // Run pre-update scripts.
            boolean hasScripts = (entity.getScripts() != null);
            if(hasScripts) {
                Beforeupdate[] beforeUpdates = entity.getScripts()
                        .getBeforeupdate();
                if(beforeUpdates != null) {
                    for(int j = 0; j < beforeUpdates.length; j++) {
                        int status = doEntityUpdate(eqlSession, res, resRecord,
                                beforeUpdates[j]);
                        if(status == EntityUpdate.SKIP_ALL) {
                            return SKIP;
                        }
                        if(status == EntityUpdate.SKIP_RECORD) {
                            return CONTINUE;
                        }
                    }
                }
            }

            // Write history.
/*
            if( entity.getHasHistoryFields().booleanValue() ) {
                resRecord = getHistoryLocalLocal().logHistory( eqlSession, res, resRecord );
            }
*/
            // Need to fix this. ILadnev.
            if(!entity.getDbobject().equals("QX_HIS_TBLFIELD")) {
                resRecord = getHistoryLocalLocal().logHistory(eqlSession, res,
                        resRecord);
            }

            // Update.
            Update update;
            if(hasScripts
                    && (update = entity.getScripts().getUpdate()) != null) {

                // Run custom update script.
                int status = doEntityUpdate(eqlSession, res, resRecord, update);
                if(status == EntityUpdate.SKIP_ALL) {
                    return SKIP;
                }
                if(status == EntityUpdate.SKIP_RECORD) {
                    return CONTINUE;
                }

            } else {

                // Call EQLAgent.
                EQLAgent agent = EQLAgentFactory.getInstance().getEQLAgent(res);
                cntAffectedRows = agent.doUpdate(eqlSession, res, resRecord);
            }

            // Update datasets.
            int size = resRecord.getDResSize();
            for(int j = 0; j < size; j++) {
                DEBUG("......try update dataset: " + j);
                __update(eqlSession, resRecord.getDRes(j), null);
            }

            // Run post-update scripts.
            if(hasScripts) {
                Afterupdate[] afterUpdates = entity.getScripts()
                        .getAfterupdate();
                if(afterUpdates != null) {
                    for(int j = 0; j < afterUpdates.length; j++) {
                        doEntityUpdate(eqlSession, res, resRecord,
                                afterUpdates[j]);
                    }
                }
            }

            // Reset record status.
            resRecord.resetStatus();

        } catch (EQLSystemException eqlex) {
            throwEQLSystemException(eqlex, entity);
        }

        // Ok. Continue update(s).
        return CONTINUE;

    }

    //
    // Does the record delete.
    //
    private int doDelete(EQLSession eqlSession, EQLERes res,
                         EQLResRecord resRecord)
            throws EQLException {

        int status = CONTINUE;

        // Get base entity.
        Entity entity = res.getEntity();

        try {

            // Run pre-delete scripts.
            boolean hasScripts = (entity.getScripts() != null);
            if(hasScripts) {
                Beforedelete[] beforeDeletes = entity.getScripts()
                        .getBeforedelete();
                if(beforeDeletes != null) {
                    for(int j = 0; j < beforeDeletes.length; j++) {
                        status = doEntityUpdate(eqlSession, res, resRecord,
                                beforeDeletes[j]);
                        if(status == EntityUpdate.SKIP_ALL) {
                            return SKIP;
                        }
                        if(status == EntityUpdate.SKIP_RECORD) {
                            return CONTINUE;
                        }
                    }
                }
            }

            // Remove referenced (fk) records.
            Fkeys[] fkeyss = entity.getFkeys();
            if(fkeyss != null) {
                for(int j = 0; j < fkeyss.length; j++) {

                    Fkeys fkeys = fkeyss[j];
                    String fkEntityName = fkeys.getFkEntity();
                    String fkColumnName = fkeys.getFkColumn();
                    String pkColumnName = fkeys.getPkColumn();
                    boolean cascadeDelete = fkeys.getCascadeDelete()
                            .booleanValue();

                    // Get primary key field name and value.
                    Efield pkField = EntityHelper.getEfieldBySrc(pkColumnName,
                            entity);
                    EQLResCell resCell = resRecord.getResCell(new EQLReqField(
                            entity, pkField));
                    if(resCell == null) {
                        throw new GenericSystemException("Primary key field '"
                                + pkField.getId() + "' value not found");
                    }
                    EQLObject pkValue = resCell.getEQLObject();

                    if(fkEntityName != null) {

                        // Remove from referenced entity.
                        Entity fkEntity = getEntityViewConfigManager()
                                .getEntityViewConfig(fkEntityName);
                        Efield fkField = EntityHelper.getEfieldBySrc(
                                fkColumnName, fkEntity);
                        doDeleteFkRecords(eqlSession, fkEntity, fkField,
                                pkValue, cascadeDelete);

                    } else {

                        // Remove from referenced table.
                        /** @todo Implement or remove it. */

                    }

                }
            }

            // Delete.
            Delete delete;

            // Run custom delete script (if any).
            if(hasScripts
                    && (delete = entity.getScripts().getDelete()) != null) {
                status = doEntityUpdate(eqlSession, res, resRecord, delete);
            }

            if(status == EntityUpdate.CONTINUE) {
                // Call EQLAgent.
                EQLAgent agent = EQLAgentFactory.getInstance().getEQLAgent(res);
                cntAffectedRows = agent.doDelete(eqlSession, res, resRecord);
            }

            // Remove record from result set.
            res.removeRecord(resRecord);

            // Run post-delete scripts.
            if(hasScripts) {
                Afterdelete[] afterDeletes = entity.getScripts()
                        .getAfterdelete();
                if(afterDeletes != null) {
                    for(int j = 0; j < afterDeletes.length; j++) {
                        doEntityUpdate(eqlSession, res, resRecord,
                                afterDeletes[j]);
                    }
                }
            }

        } catch (EQLSystemException eqlex) {
            throwEQLSystemException(eqlex, entity);
        }

        // Ok.
        return (status == EntityUpdate.SKIP_ALL ? SKIP:CONTINUE);

    }

    //
    // Do EQL delete fk entity
    //
    private void doDeleteFkRecords(EQLSession eqlSession,
                                   Entity fkEntity,
                                   Efield fkField,
                                   EQLObject pkValue,
                                   boolean cascadeDelete)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG("   remove from fk entity:");
            DEBUG("     fk entity: '" + fkEntity.getName() + "'");
            DEBUG("     fk field: '" + fkField.getName() + "'");
            DEBUG("     pk value: '" + pkValue + "'");
        }

        StringBuffer fkEqlQuery = new StringBuffer("SELECT ");
        fkEqlQuery.append(fkEntity.getName());
        fkEqlQuery.append(".* WHERE ");
        fkEqlQuery.append(fkEntity.getName()).append(".").append(
                fkField.getName());
        fkEqlQuery.append(" = ?");

        EQLIntPreparedStatement fkEqlPS = new EQLIntPreparedStatement();
        fkEqlPS.setObject(1, pkValue);

        // get all fk records (ignore datasets)
        EQLReqMetaData meta = new EQLReqMetaData();
        meta.setIgnoreAllDatasets(true);
        EQLERes fkRes = (EQLERes) doSelect(eqlSession, fkEqlQuery.toString(),
                fkEqlPS, meta);
        if(fkRes.size() == 0) {
            // nothing do delete
            return;
        }

        if(cascadeDelete) {
            // delete all fk records
            fkRes.markAllAsDelete();
            __update(eqlSession, fkRes, null);

        } else {
            // throw exception
            throw new EQLRemoveException(fkEntity, fkField, pkValue);
        }
    }

    // ========================================================= Special methods

    //
    // Get EQL session object
    //

    private EQLSession getEQLSession(LogonSession ls) {
        // build EQL session
        EQLSession eqlSession = new EQLSession();
        eqlSession.setLogonSession(ls);
        return eqlSession;
    }

    //
    // Do EQL entity afternew
    //
    private void doEntityUpdate(EQLSession eqlSession, EQLERes res,
                                EQLResRecord resRecord, Afternew afternew)
            throws EQLException {

        EntityUpdate eu = getEntityUpdate(eqlSession, afternew.getName(), res,
                resRecord);
        eu.afterNew();
    }

    //
    // Do EQL entity update
    //
    private int doEntityUpdate(EQLSession eqlSession, EQLERes res,
                               EQLResRecord resRecord, Beforeupdate update)
            throws EQLException {

        EntityUpdate eu = getEntityUpdate(eqlSession, update.getName(), res,
                resRecord);
        return eu.beforeUpdate();
    }

    //
    // Do EQL entity update
    //
    private void doEntityUpdate(EQLSession eqlSession, EQLERes res,
                                EQLResRecord resRecord, Afterupdate update)
            throws EQLException {

        EntityUpdate eu = getEntityUpdate(eqlSession, update.getName(), res,
                resRecord);

        if(update.getAsync().booleanValue()) {
            // do it async
            asyncClient.sendUpdateMessage(eu);
        } else {
            // do it sync
            eu.afterUpdate();
        }
    }

    //
    // Do EQL entity update
    //
    private int doEntityUpdate(EQLSession eqlSession, EQLERes res,
                               EQLResRecord resRecord, Update update)
            throws EQLException {

        EntityUpdate eu = getEntityUpdate(eqlSession, update.getName(), res,
                resRecord);
        return eu.update();
    }

    //
    // Do EQL entity update
    //
    private int doEntityUpdate(EQLSession eqlSession, EQLERes res,
                               EQLResRecord resRecord, Beforedelete update)
            throws EQLException {
        EntityUpdate eu = getEntityUpdate(eqlSession, update.getName(), res,
                resRecord);
        return eu.beforeDelete();
    }

    //
    // Do EQL entity update
    //
    private int doEntityUpdate(EQLSession eqlSession, EQLERes res,
                               EQLResRecord resRecord, Delete update)
            throws EQLException {

        EntityUpdate eu = getEntityUpdate(eqlSession, update.getName(), res,
                resRecord);
        return eu.delete();
    }

    //
    // Do EQL entity update
    //
    private void doEntityUpdate(EQLSession eqlSession, EQLERes res,
                                EQLResRecord resRecord, Afterdelete update)
            throws EQLException {

        EntityUpdate eu = getEntityUpdate(eqlSession, update.getName(), res,
                resRecord);

        if(update.getAsync().booleanValue()) {
            // do it async
            asyncClient.sendDeleteMessage(eu);
        } else {
            // do it sync
            eu.afterDelete();
        }
    }

    //
    // Get EQL entity update
    //
    private EntityUpdate getEntityUpdate(EQLSession eqlSession,
                                         String name,
                                         EQLERes res,
                                         EQLResRecord resRecord) {

        EntityUpdateObject euo = new EntityUpdateObject(eqlSession, res,
                resRecord);

        return EntityUpdateFactory.getEntityUpdate(name, euo);
    }

    //
    // Get Lock manager local interface
    //
    private LockManagerLocal getLockManagerLocal() {
        return (LockManagerLocal) getLocalObject(JNDINames.LockManager,
                LockManagerLocalHome.class);
    }

    //
    // Get history local interface
    //
    private HistoryLocal getHistoryLocalLocal() {
        return (HistoryLocal) getLocalObject(JNDINames.History,
                HistoryLocalHome.class);
    }

    private void throwEQLSystemException(EQLSystemException eqlex,
                                         Entity entity) {

        // Get EQL exception cause.
        Throwable t = eqlex.returnCause();

        // Exception type switch.
        if(t instanceof SQLDuplicateKeyException) {
            String message =
                    "Can't update entity '" + entity.getName()
                            + "': record with the same primary key already exists.";
            throw new EQLDuplicateKeyException(message, t);

        } else if(t instanceof SQLIndexConflictException) {
            String message =
                    "Can't update entity '" + entity.getName()
                            + "': record with the same unique index value already exists.";
            throw new EQLIndexConflictException(message, t);

        } else if(t instanceof SQLDeleteConflictException) {
            String message =
                    "Can't delete from entity '" + entity.getName()
                            + "': record is referenced by another entity.";
            throw new EQLDeleteConflictException(message, t);

        } else if(t instanceof SQLDeadlockException) {
            String message =
                    "Can't modify entity '" + entity.getName()
                            + "': database deadlock is occured.";
            throw new EQLDeadlockException(message, t);
        }

        // No special option - throw the original exception.
        throw eqlex;

    }

    // ============================================================= Inner class

    /**
     * Inner class to make any async action.
     *
     * @author [ALB] Baranov Andrey
     * @version $Revision: 1.1.1.1 $ $Date: 2005/09/12 15:30:23 $
     */

    private static class AsyncClient
            extends JMSClient {

        /**
         * Constructor
         */
        public AsyncClient() {
            super(JNDINames.JmsConnectionFactory, JNDINames.JmsAsyncQueue);
        }

        /**
         * Send async update message to message driven bean
         *
         * @param obj ASyncObject object
         */
        public void sendUpdateMessage(ASyncObject obj) {
            sendMessage(obj, EntityUpdate.UPDATE_REQUEST_FLAG);
        }

        /**
         * Send async delete message to message driven bean
         *
         * @param obj ASyncObject object
         */
        public void sendDeleteMessage(ASyncObject obj) {
            sendMessage(obj, EntityUpdate.DELETE_REQUEST_FLAG);
        }

        /**
         * Send message to message driven bean
         *
         * @param obj     ASyncObject object
         * @param request param for process method ASyncObject interface
         */
        private void sendMessage(ASyncObject obj, ASyncRequest request) {

            HashMap map = new HashMap();
            map.put(AsyncMDB.OBJECT_PARAM, obj);
            if(request != null) {
                map.put(AsyncMDB.REQUEST_PARAM, request);
            }

            send(map);
        }
    }

}
