org.getobjects.eoaccess.EODatabaseChannel.java Source code

Java tutorial

Introduction

Here is the source code for org.getobjects.eoaccess.EODatabaseChannel.java

Source

/*
  Copyright (C) 2006-2014 Helge Hess
    
  This file is part of Go.
    
  Go is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.
    
  Go 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 Lesser General Public
  License for more details.
    
  You should have received a copy of the GNU Lesser General Public
  License along with Go; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/
package org.getobjects.eoaccess;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.getobjects.eocontrol.EOAndQualifier;
import org.getobjects.eocontrol.EOFetchSpecification;
import org.getobjects.eocontrol.EOGenericRecord;
import org.getobjects.eocontrol.EOGlobalID;
import org.getobjects.eocontrol.EOKeyValueCoding;
import org.getobjects.eocontrol.EOKeyValueQualifier;
import org.getobjects.eocontrol.EOObjectTrackingContext;
import org.getobjects.eocontrol.EOQualifier;
import org.getobjects.foundation.NSDisposable;
import org.getobjects.foundation.NSException;
import org.getobjects.foundation.NSJavaRuntime;
import org.getobjects.foundation.NSKeyValueCoding;
import org.getobjects.foundation.NSKeyValueCodingAdditions;
import org.getobjects.foundation.NSObject;
import org.getobjects.foundation.UString;

/**
 * EODatabaseChannel
 * <p>
 * Important: dispose the object if you do not need it anymore.
 * <p>
 * THREAD: this object is NOT synchronized. Its considered a cheap object which
 *         can be created on demand.
 */
public class EODatabaseChannel extends NSObject implements NSDisposable, Iterator {
    protected static final Log log = LogFactory.getLog("EODatabaseChannel");
    protected static final Log perflog = LogFactory.getLog("EOPerformance");

    protected EODatabase database;
    protected EOAdaptorChannel adChannel;
    protected EOEntity currentEntity;
    protected Class currentClass;
    protected boolean isLocking;
    protected boolean fetchesRawRows;
    protected boolean makeNoSnapshots;
    protected boolean refreshObjects;
    protected EOObjectTrackingContext ec;

    protected int recordCount;
    protected Iterator<Map<String, Object>> records;
    protected Iterator<EOEnterpriseObject> objects;

    public EODatabaseChannel(final EODatabase _db) {
        this.database = _db;
    }

    /* accessors */

    public void setCurrentEntity(final EOEntity _entity) {
        this.currentEntity = _entity;
    }

    public EOEntity currentEntity() {
        return this.currentEntity;
    }

    /**
     * Determine the estimated number of results. Can be <= 0 if no estimate is
     * available from the database.
     * 
     * @return number of records in the result set
     */
    public int recordCount() {
        return this.recordCount;
    }

    /* operations */

    /**
     * Provides access to the underlying EOAdaptorChannel which is used for the
     * fetch. This only returns a result when a fetch is in progress.
     * 
     * @return The adaptor channel used for the current fetch.
     */
    public EOAdaptorChannel adaptorChannel() {
        return this.adChannel;
    }

    /**
     * Returns whether the channel is currently part of a transaction.
     * 
     * @return true if a transaction is in progress, no otherwise
     */
    public boolean isInTransaction() {
        return this.adChannel != null ? this.adChannel.isInTransaction() : false;
    }

    /**
     * Begins a database transaction. This allocates an adaptor channel which will
     * be shared by subsequent fetches/operations. The channel is checked back
     * into the pool on the next rollback/commit.
     * <p>
     * Be sure to always commit or rollback the transaction!
     * 
     * @return null if everything went fine, an exception otherwise
     */
    public Exception begin() {
        if (this.isInTransaction()) {
            log.error("attempted a nested transaction!");
            return new NSException("transaction already in progress");
        }

        if (this.adChannel == null) {
            if ((this.adChannel = this.acquireChannel()) == null)
                return new NSException("could not acquire a channel");
        }

        Exception error = this.adChannel.begin();
        if (error == null) /* everything was fine, we keep the channel open */
            return null;

        /* if we could not begin a tx, we always close the channel */
        this.releaseChannel();
        return error;
    }

    /**
     * Commits a database transaction. This also releases the associated adaptor
     * channel back to the connection pool.
     * 
     * @return null if everything went fine, an exception otherwise
     */
    public Exception commit() {
        if (this.adChannel == null) {/* not considered an error, nothing happened */
            log.warn("called commit w/o an open channel");
            return null;
        }

        Exception error = this.adChannel.commit();
        this.releaseChannel();
        return error;
    }

    /**
     * Rolls back a database transaction. This also releases the associated
     * adaptor channel back to the connection pool.
     * 
     * @return null if everything went fine, an exception otherwise
     */
    public Exception rollback() {
        if (this.adChannel == null) {/* not considered an error, nothing happened */
            log.warn("called rollback w/o an open channel");
            return null;
        }

        Exception error = this.adChannel.rollback();
        this.releaseChannel();
        return error;
    }

    /**
     * Creates a map where the keys are level-1 relationship names and the values
     * are subpathes. The method also resolves flattened relationships (this is,
     * if a relationship is a flattened one, the keypath of the relationship
     * will get processed).
     * <p>
     * Pathes:
     * <pre>
     *   toCompany.toProject
     *   toCompany.toEmployment
     *   toProject</pre>
     * Will result in:
     * <pre>
     *   {
     *     toCompany = [ toProject, toEmployment ];
     *     toProject = [];
     *   }</pre>
     * Note that the keys are never flattened relationships.
     * 
     * @param _entity the base entity
     * @param _pathes the pathes which got passed in by the EOFetchSpecification
     * @return a Map as described
     */
    protected Map<String, List<String>> levelPrefetchSpecificiation(final EOEntity _entity,
            final String[] _pathes) {
        if (_entity == null || _pathes == null || _pathes.length == 0)
            return null;

        final Map<String, List<String>> level = new HashMap<String, List<String>>(8);

        for (String path : _pathes) {
            String relname;
            int dotidx;

            /* split off first part of relationship */

            dotidx = path.indexOf('.');
            relname = (dotidx >= 0) ? path.substring(0, dotidx) : path;

            EORelationship rel = _entity.relationshipNamed(relationshipNameWithoutParameters(relname));
            if (rel == null) {
                log.error("did not find specified prefetch relationship '" + path + "' in entity: '"
                        + _entity.name() + "'");
                continue;
            }

            /* process flattened relationships */

            if (rel.isFlattened()) {
                path = rel.relationshipPath();

                dotidx = path.indexOf('.');
                relname = (dotidx >= 0) ? path.substring(0, dotidx) : path;

                if ((rel = _entity.relationshipNamed(relname)) == null) {
                    log.error("did not find specified first relationship " + "of flattened prefetch: " + path);
                    continue;
                }
            }

            /* process relationship */

            final EOJoin[] joins = rel.joins();
            if (joins == null || joins.length == 0) {
                log.warn("prefetch relationship has no joins, ignoring: " + rel);
                continue;
            }
            if (joins.length > 1) {
                log.warn("prefetch relationship has multiple joins (unsupported), " + "ignoring: " + rel);
                continue;
            }

            /* add relation names to map */

            List<String> sublevels = level.get(relname);
            if (sublevels == null) {
                sublevels = new ArrayList<String>(1);
                level.put(relname, sublevels);
            }

            if (dotidx >= 0)
                sublevels.add(path.substring(dotidx + 1));
        }

        return level;
    }

    /**
     * Given a list of relationship pathes this method extracts the set of
     * level-1 flattened relationships.
     * 
     * Eg:
     *   customers.address
     *   phone.number
     * where customers is a flattened but phone is not, will return:
     *   customers
     * 
     * @param  _entity the base entity used to lookup the EORelationship
     * @param  _pathes an array of key pathes
     * @return the list of flattened relationships or null if there was none
     */
    public List<String> flattenedRelationships(final EOEntity _entity, final String[] _pathes) {
        if (_entity == null || _pathes == null || _pathes.length == 0)
            return null;

        List<String> flattened = null;
        for (String path : _pathes) {
            /* split off first part of relationship */

            final int dotidx = path.indexOf('.');
            String relname = (dotidx >= 0) ? path.substring(0, dotidx) : path;

            /* split off fetch parameters */

            relname = relationshipNameWithoutParameters(relname);

            /* lookup relationship */

            final EORelationship rel = _entity.relationshipNamed(relname);
            if (rel == null || !rel.isFlattened())
                continue;

            if (flattened == null)
                flattened = new ArrayList<String>(4);
            flattened.add(relname); // TBD: do we need the name with parameters?
        }
        return flattened;
    }

    public EOAttribute[] selectListForFetchSpecification(final EOEntity _entity, final EOFetchSpecification _fs) {
        final String[] fetchKeys = _fs != null ? _fs.fetchAttributeNames() : null;

        if (fetchKeys == null) {
            if (_entity != null)
                return _entity.attributes();
            return null; /* will trigger '*' fetch? */
        }

        if (_entity != null)
            return _entity.attributesWithNames(fetchKeys);

        // TBD: generate EOAttrs for fetchKeys
        return null;
    }

    /**
     * This method prepares the channel for a fetch and initiates the fetch. Once
     * called, the channel has various instance variables configured and the
     * results can be retrieved using fetchObject() or fetchRow().
     * <p>
     * This is different to selectObjectsWithFetchSpecification(), which has
     * additional handling for prefetched relationships.
     * 
     * @param _fs - The EOFetchSpecification which outlines how objects are being
     *   fetched.
     * @param _ec - the EOObjectTrackingContext for the fetch
     * @return Null if everything went fine, or an exception object representing
     *   the error.
     */
    public Exception primarySelectObjectsWithFetchSpecification(final EOFetchSpecification _fs,
            final EOObjectTrackingContext _ec) {
        final boolean isDebugOn = log.isDebugEnabled();
        final boolean perfOn = perflog.isDebugEnabled();

        if (isDebugOn)
            log.debug("primary select: " + _fs);
        if (perfOn)
            perflog.debug("primarySelectObjectsWithFetchSpecification ..");

        /* tear down */
        this.cancelFetch();

        /* prepare */

        this.isLocking = _fs.locksObjects();
        this.fetchesRawRows = _fs.fetchesRawRows();
        this.ec = _ec;

        this.setCurrentEntity(this.database.entityNamed(_fs.entityName()));
        if (this.currentEntity == null && !this.fetchesRawRows) {
            log.error("missing entity named: " + _fs.entityName());
            return new Exception("did not find entity for fetch!");
        }

        final EOAttribute[] selectList = this.selectListForFetchSpecification(this.currentEntity, _fs);

        if (isDebugOn) {
            log.debug("  entity: " + this.currentEntity.name());
            log.debug("  attrs:  " + UString.componentsJoinedByString(selectList, ","));
        }

        this.makeNoSnapshots = false;
        if (this.currentEntity != null && this.currentEntity.isReadOnly())
            this.makeNoSnapshots = true;
        else if (_fs.fetchesReadOnly())
            this.makeNoSnapshots = true;

        /* determine object class */

        if (!this.fetchesRawRows) {
            // TBD: support per-object classes by setting this to null if the
            //      entity says its multi-class
            this.currentClass = this.database.classForEntity(this.currentEntity);
        }

        /* setup */

        boolean didOpenChannel = false;
        Exception error = null;
        try {
            if (this.adChannel == null) {
                this.adChannel = this.acquireChannel();
                didOpenChannel = true;
            }
            if (this.adChannel == null) // TODO: improve error
                return new NSException("could not create adaptor channel");

            /* perform fetch */

            List<Map<String, Object>> results;

            /* Note: custom queries are detected by the adaptor */
            if (perfOn)
                perflog.debug("  selectAttributes ..");
            results = this.adChannel.selectAttributes(null, // selectList, /* was null to let the channel do the work, why? */
                    _fs, this.isLocking, this.currentEntity);
            if (perfOn)
                perflog.debug("  did selectAttributes.");

            if (results == null) {
                log.error("could not perform adaptor query: ", this.adChannel.lastException);
                return this.adChannel.consumeLastException();
            }

            this.recordCount = results.size();
            this.records = results.iterator();
        } catch (Exception e) {
            error = e;
            // TBD: We should release with the error? (so that the pool does not reuse
            //      a connection with errors)
        } finally {
            if (didOpenChannel)
                this.releaseChannel();
        }

        if (perfOn)
            perflog.debug("primarySelectObjectsWithFetchSpecification.");
        return error;
    }

    /**
     * This method prepares the channel for a fetch and initiates the fetch. Once
     * called, the channel has various instance variables configured and the
     * results can be retrieved using fetchObject() or fetchRow().
     * <p>
     * This is the primary method for fetches and has additional handling for
     * prefetched relationships.
     * 
     * @param _fs The EOFetchSpecification which outlines how objects are being
     *   fetched.
     * @param _ec TODO
     * @return Null if everything went fine, or an exception object representing
     *   the error.
     */
    public Exception selectObjectsWithFetchSpecification(final EOFetchSpecification _fs,
            final EOObjectTrackingContext _ec) {
        final String[] prefetchRelPathes = _fs != null ? _fs.prefetchingRelationshipKeyPaths() : null;

        /* simple case, no prefetches */

        if (prefetchRelPathes == null || prefetchRelPathes.length == 0)
            return this.primarySelectObjectsWithFetchSpecification(_fs, _ec);

        final boolean isDebugOn = log.isDebugEnabled();
        if (isDebugOn) {
            log.debug("select with prefetch: " + UString.componentsJoinedByString(prefetchRelPathes, ", "));
        }

        /* Prefetches were specified, process them. We open a channel and a 
         * transaction.
         */

        List<EOEnterpriseObject> baseObjects = null;
        Exception error = null;
        boolean didOpenChannel = false;
        try {
            if (this.adChannel == null) {
                this.adChannel = this.acquireChannel();
                didOpenChannel = true;

                if ((error = this.begin()) != null)
                    return error;
            }
            if (this.adChannel == null) // TODO: improve error
                return new NSException("could not create adaptor channel");

            /* First we fetch all primary objects and collect them in a List */

            error = this.primarySelectObjectsWithFetchSpecification(_fs, _ec);
            if (error != null) {
                this.cancelFetch(); /* better be sure ;-) */
                return error; /* initial fetch failed */
            }

            baseObjects = new ArrayList<EOEnterpriseObject>(this.recordCount);
            EOEnterpriseObject o;
            while ((o = (EOEnterpriseObject) this.fetchObject()) != null) {
                baseObjects.add(o);
                // TBD: already extract something?
            }
            this.cancelFetch();

            if (isDebugOn)
                log.debug("fetched objects: " + baseObjects.size());

            /* Then we fetch relationships for the 'baseObjects' we just fetched. */

            error = this.fetchRelationships(_fs.entityName(), prefetchRelPathes, baseObjects, _ec);
            if (error != null)
                return error;
        } finally {
            if (didOpenChannel) {
                /* Note: We do not commit because we just fetched stuff and commits
                 *       increase the likeliness that something fails. So: rollback in
                 *       both ways.
                 */
                this.rollback(); // TBD: do we actually care about the result?

                this.releaseChannel();
            }
        }

        /* set the result */

        if (error == null) {
            if (isDebugOn)
                log.debug("assigned result objects: " + baseObjects.size());

            this.objects = baseObjects.iterator();
        } else if (isDebugOn)
            log.debug("did not set result set because an error is set", error);

        return error;
    }

    /**
     * Prefetches a set of related objects.
     * 
     * @param _entityName        - entity this fetch is relative to
     * @param _prefetchRelPathes - the pathes we want to prefetch
     * @param _baseObjects       - the set of objects we want to prefetch for
     * @param _ec                - the active tracking context
     * @return an Exception if an error occurred, null on success
     */
    public Exception fetchRelationships(final String _entityName, final String[] _prefetchRelPathes,
            List<EOEnterpriseObject> _baseObjects, EOObjectTrackingContext _ec) {
        if (_prefetchRelPathes == null || _prefetchRelPathes.length == 0)
            return null /* no error */;
        if (_baseObjects == null || _baseObjects.size() == 0)
            return null /* no error */;

        /* entity */

        final EOEntity entity = this.database.entityNamed(_entityName);
        if (entity == null) { /* Note: should have failed before */
            log.error("missing entity named: " + _entityName);
            return new Exception("did not find entity for fetch!");
        }

        /* process relationships (key is a level1 name, value are the subpaths) */

        final Map<String, List<String>> leveledPrefetches = this.levelPrefetchSpecificiation(entity,
                _prefetchRelPathes);

        /*
         * Maps/caches Lists of values for a given attribute in the base result.
         * Usually the key is the primary key.
         * 
         * We cache this because its most likely reused when we have multiple
         * prefetches. The fetch will most usually go against the primary key ...
         */
        final EODatabaseChannelFetchHelper helper = new EODatabaseChannelFetchHelper(_baseObjects);

        for (String relName : leveledPrefetches.keySet()) {
            /* The relName is never a path, its a level-1 key. the value in
             * leveledPrefetches contains 'subpathes'.
             */
            Exception error = this.fetchRelationship(entity, relName, _baseObjects, leveledPrefetches.get(relName),
                    helper, _ec);

            if (error != null)
                return error;
        }

        /* fetch flattened relationships (NOT IMPLEMENTED) */

        final List<String> flattenedRelationships = this.flattenedRelationships(entity, _prefetchRelPathes);
        if (flattenedRelationships != null) {
            for (String rel : flattenedRelationships) {
                EORelationship flattenedRel = entity.relationshipNamed(rel);

                // TBD: process flattened relationships (walk over initial set)
                log.warn("not processing flattened relationship: " + flattenedRel);
            }
        }

        return null; /* no error */
    }

    /**
     * Cleans the name of the relationship from parameters, eg the name contain
     * repeaters like '*' (eg parent* => repeat relationship 'parent' until no
     * objects are found anymore).
     * 
     * @param _name - name of the relationship, eg 'employments' or 'parent*'
     * @return cleaned relationship name, eg 'employments' or 'parent'
     */
    public static String relationshipNameWithoutParameters(final String _name) {
        if (_name == null)
            return null;

        /* cut off '*' (releationship fetch repeaters like parent*) */
        return _name.endsWith("*") ? _name.substring(0, _name.length() - 1) : _name;
    }

    /**
     * This is the master of desaster which performs the actual fetch of the
     * relationship for a given set of 'baseObjects'.
     * <p>
     * <ul>
     *   <li>relationship names can contain a repeat '*' parameter,
     *       eg 'parentDocument*'</li>
     * </ul>
     * 
     * @param _entity - the entity of the *base* objects
     * @param _relationNameWithParameters - the name of the relationship
     * @param _baseObjects - the objects which we want to fetch the relship for 
     * @param _prefetchPathes - subpathes we want to prefetch
     * @param _helper - a fetch context to track objects during the fetch
     * @param _ec - the associated editing-context, if there is one
     * @return
     */
    protected Exception fetchRelationship(final EOEntity _entity, final String _relationNameWithParameters,
            final List<EOEnterpriseObject> _baseObjects, List<String> _prefetchPathes,
            final EODatabaseChannelFetchHelper _helper, final EOObjectTrackingContext _ec) {
        /* Note: EODatabaseContext.batchFetchRelationship */

        /* first we check whether the relationship contains a repeat-parameter,
         * eg:
         *   parent*
         * This means that we prefetch 'parent' again and again (until it returns
         * no 'baseObjects' for a subsequent fetch).
         */

        String relName = _relationNameWithParameters;
        if (relName != null && relName.endsWith("*")) {
            relName = relationshipNameWithoutParameters(_relationNameWithParameters);

            /* fixup prefetch patches to include our tree-depth fetch */
            if (!_prefetchPathes.contains(_relationNameWithParameters)) {
                _prefetchPathes = new ArrayList<String>(_prefetchPathes);
                _prefetchPathes.add(_relationNameWithParameters);
            }
        }

        /* Note: we filter out non-1-join relationships in the levelXYZ() method */
        final EORelationship rel = _entity.relationshipNamed(relName);
        final EOJoin join = rel.joins()[0];
        if (join == null) {
            log.error("did not find a join in relationship: " + relName);
            return null;
        }

        /* extract values of source object list for IN query on target */

        final String srcName = join.sourceAttribute().name();
        final List<Object> srcValues = _helper.getSourceValues(srcName);

        /* This is a Map which maps the join target-value to matching
         * EOs. Usually its just one.
         */
        final Map<Object, List<EOEnterpriseObject>> valueToObjects = _helper.getValueToObjects(srcName);

        // TBD: srcValues could be empty?! Well, values could be NULL (for non-pkey
        //      source attributes).

        /* construct fetch specification */
        // Note: uniquing should be done in fetchObject using an editing
        //       context which is passed around

        // TBD: we also need to include the join. Otherwise we might fetch
        //      values from other entities using the same join table.
        //      (OGo: company_assignment used for team->account + e->person)
        //      hm, wouldn't that be toSource.id IN ?!
        //      TBD: do we really need that? The example is crap because the
        //           join uses 'company' as a base. (properly modelled the
        //           two would be stored in different tables!)
        //        Update: Same issue with project_company_assignment
        // TBD: we should only fetch INs which we do not already have cached
        //      .. in a per transaction uniquer
        //      TBD: well, this is the editing context. we have this now, but we
        //           refetch objects which is stupid
        //      TBD: do we really need to support arbitrary targets or can we
        //           use EOKeyGlobalID?
        final EOAttribute targetAttr = join.destinationAttribute();
        if (targetAttr == null) {
            log.error("did not find target-attr of relationship join: " + rel + ": " + join);
            return null; // TBD: hm ... (eg if the model is b0rked)
        }
        final String targetName = targetAttr.name();

        EOFetchSpecification fs = null;
        if (true) {
            EOQualifier joinQualifier = new EOKeyValueQualifier(targetName,
                    EOQualifier.ComparisonOperation.CONTAINS, srcValues);

            // TBD: we should batch?
            fs = new EOFetchSpecification(rel.destinationEntity().name(), joinQualifier, null /* ordering */);
        } else {

        }
        if (_prefetchPathes != null && _prefetchPathes.size() > 0) {
            /* apply nested prefetches */
            fs.setPrefetchingRelationshipKeyPaths(_prefetchPathes.toArray(new String[0]));
        }

        /* run nested query */

        final boolean isDebugOn = log.isDebugEnabled();
        Exception error;
        if ((error = this.selectObjectsWithFetchSpecification(fs, _ec)) != null) {
            this.cancelFetch(); /* better be sure ;-) */
            return error; // rollback must be handled in caller
        }

        if (isDebugOn)
            log.debug("process rel results ...");

        Object relObject;
        while ((relObject = this.fetchObject()) != null) {
            /* targetName is the target attribute in the join */
            Object v = ((NSKeyValueCoding) relObject).valueForKey(targetName);

            /* this is the list of join source objects which have that value
             * in the source attribute of the join.
             */
            List<EOEnterpriseObject> srcObjects = valueToObjects.get(v);
            if (srcObjects == null) {
                /* I think this can only happen when concurrent transactions
                 * delete items.
                 * Hm, which would be an error, because the source object would
                 * have a key, but wouldn't be hooked up?!
                 */
                log.warn("found no objects to hook up for foreign key: " + v);
                continue;
            }
            if (isDebugOn) {
                log.debug("    -> rel target: " + relObject);
                log.debug("       join value: " + v);
                log.debug("       sources:   #" + srcObjects.size() + ": " + srcObjects);
            }

            /* Hook up the fetched relationship target objects with the objects which
             * link to it.
             */
            boolean isRelEO = relObject instanceof EORelationshipManipulation;
            if (isRelEO) {
                for (EOEnterpriseObject srcObject : srcObjects) {
                    log.debug("         hook up two-way: " + relName);
                    srcObject.addObjectToBothSidesOfRelationshipWithKey((EORelationshipManipulation) relObject,
                            relName);
                }
            } else {
                for (EOEnterpriseObject srcObject : srcObjects) {
                    log.debug("         hook up one-way: " + relName);
                    srcObject.addObjectToPropertyWithKey(relObject, relName);
                }
            }
        }

        return null; /* fetch done */
    }

    /**
     * Finishes a fetch by resetting transient fetch state in the channel. This
     * is automatically called when fetchRow() returns no object and should
     * always be called if a fetch is stopped before all objects got retrieved.
     */
    public void cancelFetch() {
        /* Note: do not release the adaptor channel in here! */
        this.ec = null;
        this.objects = null;
        this.records = null;
        this.currentEntity = null;
        this.recordCount = 0;
        this.isLocking = false;
        this.fetchesRawRows = false;
        this.currentClass = null;
    }

    /**
     * Whether or not a fetch is in progress (a select was done and objects can
     * be retrieved using a sequence of fetchObject() calls).
     * 
     * @return true if objects can be fetched, false if no fetch is in progress
     */
    public boolean isFetchInProgress() {
        return this.records != null && this.objects != null;
    }

    /* fetching */

    /**
     * Fetches the next row from the database. Currently we fetch all rows once
     * and then step through the resultset ...
     * 
     * @return a Map containing the next record, or null if there are no more
     */
    public Map<String, Object> fetchRow() {
        if (this.records == null)
            return null;

        if (!this.records.hasNext()) {
            this.cancelFetch();
            return null;
        }

        return this.records.next();
    }

    /**
     * This is called when 'this.currentClass' is set to null. It to support
     * different classes per entity where the class will be selected based on
     * the row.
     * 
     * @param _row - database record
     * @return the EO class to instantiate for the row
     */
    protected Class objectClassForRow(final Map<String, Object> _row) {
        // TBD: implement. Add additional class information to EOEntity ...
        return EOGenericRecord.class; // non-sense
    }

    /**
     * This is the primary method to retrieve an object after a select().
     * 
     * @return null if there are no more objects, or the fetched object/record
     */
    public Object fetchObject() {
        final boolean isDebugOn = log.isDebugEnabled();

        if (isDebugOn)
            log.debug("  fetch object ...");

        /* use iterator if the objects are already fetched in total */

        if (this.objects != null) {
            if (!this.objects.hasNext()) {
                if (isDebugOn)
                    log.debug("    return no object, finished fetching.");
                this.cancelFetch();
                return null;
            }

            Object nextObject = this.objects.next();
            if (isDebugOn)
                log.debug("    return avail object: " + nextObject);
            return nextObject;
        }

        /* fetch raw row from adaptor channel */

        final Map<String, Object> row = this.fetchRow();
        if (row == null) {
            if (isDebugOn)
                log.debug("    return no row, finished fetching.");
            // TBD: should we cancel?
            return null;
        }
        if (this.fetchesRawRows) {
            if (isDebugOn)
                log.debug("    return raw row: " + row);
            return row;
        }

        Class clazz = this.currentClass != null // TBD: other way around?
                ? this.currentClass
                : this.objectClassForRow(row);
        ;

        if (this.currentClass == null) { // TBD: retrieve dynamically
            log.warn("    missing class, return raw row: " + row);
            return row;
        }

        // TODO: we might want to do uniquing here ..

        final EOGlobalID gid = (this.currentEntity != null) ? this.currentEntity.globalIDForRow(row) : null;

        if (!this.refreshObjects && this.ec != null) {
            // TBD: we could ask some delegate whether we should refresh
            Object oldEO = this.ec.objectForGlobalID(gid);
            if (oldEO != null)
                return oldEO; /* was already fetched/registered */
        }
        // TBD: we might still want to *reuse* the object (might have additional
        //      non-persistent information attached)

        /* instantiate new object */

        final Object eo = NSJavaRuntime.NSAllocateObject(clazz, EOEntity.class, this.currentEntity);
        if (eo == null) {
            log.error("failed to allocate EO: " + clazz + ": " + row);
            return null;
        }

        if (isDebugOn)
            log.debug("    allocated: " + eo);

        /* apply row values */

        final Set<String> keys = row.keySet();

        if (eo instanceof EOKeyValueCoding) {
            if (isDebugOn)
                log.debug("    push row: " + row);
            EOKeyValueCoding eok = (EOKeyValueCoding) eo;

            for (String attributeName : keys)
                eok.takeStoredValueForKey(row.get(attributeName), attributeName);

            if (isDebugOn)
                log.debug("    filled: " + eo);
        } else {
            // TODO: call default implementation
            log.error("attempt to construct a non-EO, not yet implemented: " + eo);
        }

        /* register in editing context */

        if (gid != null && this.ec != null)
            this.ec.recordObject(eo, gid);

        /* awake objects */

        if (eo != null) {
            // TBD: this might be a bit early since relationships are not yet fetched
            if (eo instanceof EOEnterpriseObject) {
                if (isDebugOn)
                    log.debug("    awake ...: " + eo);
                ((EOEnterpriseObject) eo).awakeFromFetch(this.database);
            }
        }

        /* make snapshot */

        if (!this.makeNoSnapshots) {
            /* Why don't we just reuse the row? Because applying the row on the object
             * might have changed or cooerced values which would be incorrectly
             * reported as changes later on.
             * 
             * We make the snapshot after the awake for the same reasons.
             */

            Map<String, Object> snapshot = null;
            if (eo instanceof EOKeyValueCoding) {
                if (isDebugOn)
                    log.debug("    make snapshot ...");
                EOKeyValueCoding eok = (EOKeyValueCoding) eo;

                snapshot = new HashMap<String, Object>(keys.size());
                for (String attributeName : keys)
                    snapshot.put(attributeName, eok.storedValueForKey(attributeName));
            }

            /* record snapshot */

            if (snapshot != null) {
                if (eo instanceof EOActiveRecord)
                    ((EOActiveRecord) eo).setSnapshot(snapshot);
                // else: do record a snapshot in the editing or database context
            }
        }

        if (isDebugOn)
            log.debug("  = fetched object: " + eo);
        return eo;
    }

    /* database operations (handled by EODatabaseContext in EOF) */

    /**
     * The method converts the database operations into a set of adaptor
     * operations which are then performed using the associated
     * EOAdaptorChannel.
     */
    public Exception performDatabaseOperations(EODatabaseOperation[] _ops) {
        if (_ops == null || _ops.length == 0)
            return null; /* nothing to do */

        /* turn db ops into adaptor ops */

        final List<EOAdaptorOperation> aops = this.adaptorOperationsForDatabaseOperations(_ops);
        if (aops == null || aops.size() == 0)
            return null; /* nothing to do */

        /* perform adaptor ops */

        boolean didOpenChannel = false;
        Exception error = null;
        try {
            if (this.adChannel == null) {
                this.adChannel = this.acquireChannel();
                didOpenChannel = true;
            }

            if (this.adChannel == null) // TODO: improve error
                return new NSException("could not create adaptor channel");

            // TBD: should we open a transaction? => only for >1 ops?
            // here we assume that the adChannel does TX which it currently does
            // not! ;-)
            error = this.adChannel.performAdaptorOperations(aops);
        } catch (Exception e) {
            error = e;
        } finally {
            if (didOpenChannel)
                this.releaseChannel();
        }

        return error;
    }

    /**
     * This method creates the necessary EOAdaptorOperation's for the given
     * EODatabaseOperation's and attaches them to the respective database-op
     * objects.
     * 
     * @param _ops - array of EODatabaseOperation's
     * @return List of EOAdaptorOperation's
     */
    protected List<EOAdaptorOperation> adaptorOperationsForDatabaseOperations(final EODatabaseOperation[] _ops) {
        if (_ops == null || _ops.length == 0)
            return null; /* nothing to do */

        final List<EOAdaptorOperation> aops = new ArrayList<EOAdaptorOperation>(4);

        for (int i = 0; i < _ops.length; i++) {
            EOEntity entity = _ops[i].entity();
            EOAdaptorOperation aop = new EOAdaptorOperation(entity);

            int dbop = _ops[i].databaseOperator();
            if (dbop == 0) {
                if (_ops[i].object() instanceof EOActiveRecord) {
                    EOActiveRecord eo = (EOActiveRecord) _ops[i].object();
                    if (eo.isNew())
                        dbop = EOAdaptorOperation.AdaptorInsertOperator;
                    else if (eo.hasChanges())
                        dbop = EOAdaptorOperation.AdaptorUpdateOperator;
                }
            }
            if (dbop == 0)
                log.warn("got no operator in DBOp: " + _ops[i]);
            aop.setAdaptorOperator(dbop);

            if (entity == null) {
                log.warn("entity is missing in database operation: " + _ops[i]);
                if (this.database != null)
                    entity = this.database.entityForObject(_ops[i].object());
            }

            switch (_ops[i].databaseOperator()) {
            case EOAdaptorOperation.AdaptorDeleteOperator: {
                // TODO: do we also want to add attrs used for locking?
                final Map<String, Object> snapshot = _ops[i].dbSnapshot();
                EOQualifier pq = null;

                if (entity == null)
                    log.error("missing entity, cannot calculate delete op: " + _ops[i]);
                else if (snapshot == null)
                    pq = entity.qualifierForPrimaryKey(_ops[i].object());
                else
                    pq = entity.qualifierForPrimaryKey(snapshot);

                if (pq == null) {
                    log.error("could not calculate primary key qualifier for op: " + _ops[i]);
                    throw new NSException("could not determine primary-key qualifier!");
                }
                aop.setQualifier(pq);
                break;
            }

            case EOAdaptorOperation.AdaptorInsertOperator: {
                final Map<String, Object> values = NSKeyValueCodingAdditions.Utility.valuesForKeys(_ops[i].object(),
                        entity.classPropertyNames());
                aop.setChangedValues(values); // Note: does not copy the Map!

                _ops[i].setNewRow(values);

                // TODO: we need to know our new primary key for auto-increment keys!
                break;
            }

            case EOAdaptorOperation.AdaptorUpdateOperator:
                Map<String, Object> snapshot = _ops[i].dbSnapshot();

                /* calculate qualifier */

                EOQualifier pq = null;
                if (entity == null)
                    /* we could try to construct a full qualifier over all fields? */
                    log.error("missing entity, cannot calculate update op: " + _ops[i]);
                else if (snapshot == null)
                    pq = entity.qualifierForPrimaryKey(_ops[i].object());
                else
                    pq = entity.qualifierForPrimaryKey(snapshot);

                if (pq == null) {
                    log.error(
                            "could not calculate primary key qualifier for " + "operation, snapshot: " + snapshot);
                    throw new NSException("could not determine primary-key qualifier!");
                }

                EOAttribute[] lockAttrs = entity.attributesUsedForLocking();
                if (lockAttrs != null && lockAttrs.length > 0 && snapshot != null) {
                    EOQualifier[] qs = new EOQualifier[lockAttrs.length + 1];
                    qs[0] = pq;
                    for (int j = 1; j < lockAttrs.length; j++) {
                        String name = lockAttrs[j - 1].name();
                        if (name == null)
                            name = lockAttrs[j - 1].columnName();
                        qs[j] = new EOKeyValueQualifier(name, snapshot.get(name));
                    }
                    pq = new EOAndQualifier(qs);
                }

                aop.setQualifier(pq);

                /* calculate changed values */

                Map<String, Object> values = null;

                if (_ops[i].object() instanceof EOEnterpriseObject) {
                    EOEnterpriseObject eo = (EOEnterpriseObject) _ops[i].object();
                    if (snapshot != null)
                        values = eo.changesFromSnapshot(snapshot);
                    else {
                        /* no snapshot, need to update everything */
                        values = NSKeyValueCodingAdditions.Utility.valuesForKeys(_ops[i].object(),
                                entity.classPropertyNames());
                    }
                } else {
                    log.warn("object for update is not an EOEnterpriseObject");
                    /* for other objects we just update everything */
                    values = NSKeyValueCodingAdditions.Utility.valuesForKeys(_ops[i].object(),
                            entity.classPropertyNames());
                    // TODO: changes might include non-class props (like assocs)
                }

                if (values == null || values.size() == 0) {
                    if (log.isInfoEnabled())
                        log.info("no values to update: " + _ops[i]);
                    aop = null;
                } else {
                    aop.setChangedValues(values);

                    /* Note: we need to copy the snapshot because we might ignore it in
                     *       case the dbop fails.
                     */
                    if (snapshot != null && snapshot != values) {
                        snapshot = new HashMap<String, Object>(snapshot);
                        snapshot.putAll(values); /* overwrite old values */
                    }

                    _ops[i].setDBSnapshot(snapshot);
                }
                break;

            default:
                log.warn("unsupported database operation: " + _ops[i]);
                aop = null;
                break;
            }

            if (aop != null) {
                aops.add(aop);
                _ops[i].addAdaptorOperation(aop);
            }
        }
        return aops;
    }

    /* dispose */

    public void dispose() {
        this.cancelFetch();
        this.releaseChannel();
        this.database = null;
    }

    /* iterator */

    public boolean hasNext() {
        if (this.records != null)
            return this.records.hasNext();
        if (this.objects != null)
            return this.objects.hasNext();
        return false;
    }

    public Object next() {
        return this.fetchObject();
    }

    public void remove() {
        throw new UnsupportedOperationException("EODatabaseChannel does not support remove");
    }

    /* channel */

    protected EOAdaptorChannel acquireChannel() {
        if (this.database == null) {
            log.warn("channel has no database set: " + this);
            return null;
        }

        EOAdaptor adaptor = this.database.adaptor();
        if (adaptor == null) {
            log.warn("database has no adaptor set: " + this);
            return null;
        }

        log.info("opening adaptor channel ...");
        return adaptor.openChannelFromPool();
    }

    /**
     * Internal method to release the EOAdaptorChannel (put it back into the
     * connection pool).
     */
    protected void releaseChannel() {
        if (this.adChannel != null) {
            EOAdaptor adaptor = null;

            if (this.database != null)
                adaptor = this.database.adaptor();

            if (log.isInfoEnabled())
                log.info("releasing adaptor channel: " + this.adChannel);

            if (adaptor != null)
                adaptor.releaseChannel(this.adChannel);
            else
                this.adChannel.dispose();
            this.adChannel = null;
        }
    }

    /* description */

    @Override
    public void appendAttributesToDescription(final StringBuilder _d) {
        super.appendAttributesToDescription(_d);

        if (this.database != null)
            _d.append(" db=" + this.database);
        if (this.adChannel != null)
            _d.append(" channel=" + this.adChannel);

        if (this.objects != null) {
            _d.append(" objects");
            _d.append(this.objects.hasNext() ? "-more" : "-done");
        }
    }

    /* helper class */

    static class EODatabaseChannelFetchHelper extends NSObject {
        Map<String, List<Object>> sourceKeyToValues;
        Map<String, Map<Object, List<EOEnterpriseObject>>> sourceKeyToValueToObjects;
        List<EOEnterpriseObject> baseObjects;

        public EODatabaseChannelFetchHelper(List<EOEnterpriseObject> _baseObjects) {
            this.sourceKeyToValues = new HashMap<String, List<Object>>(4);
            this.sourceKeyToValueToObjects = new HashMap<String, Map<Object, List<EOEnterpriseObject>>>(4);

            this.baseObjects = _baseObjects;
        }

        public List<Object> getSourceValues(final String srcName) {
            List<Object> result = this.sourceKeyToValues.get(srcName);
            if (result == null) {
                this.fill(srcName);
                result = this.sourceKeyToValues.get(srcName);
            }
            return result;
        }

        public Map<Object, List<EOEnterpriseObject>> getValueToObjects(final String srcName) {
            Map<Object, List<EOEnterpriseObject>> result = this.sourceKeyToValueToObjects.get(srcName);
            if (result == null) {
                this.fill(srcName);
                result = this.sourceKeyToValueToObjects.get(srcName);
            }
            return result;
        }

        protected void fill(final String srcName) {
            List<Object> srcValues;
            Map<Object, List<EOEnterpriseObject>> valueToObjects;

            /* not yet cached, calculate */
            srcValues = new ArrayList<Object>(256);
            valueToObjects = new HashMap<Object, List<EOEnterpriseObject>>(256);

            this.sourceKeyToValues.put(srcName, srcValues);
            this.sourceKeyToValueToObjects.put(srcName, valueToObjects);

            if (this.baseObjects == null)
                return;

            /* calculate */

            for (EOEnterpriseObject baseObject : this.baseObjects) {
                Object v = ((NSKeyValueCoding) baseObject).valueForKey(srcName);
                if (v == null)
                    continue;

                /* Most often the source key is unique and we have just one
                 * entry, but its not a strict requirement
                 */
                List<EOEnterpriseObject> vobjects = valueToObjects.get(v);
                if (vobjects == null) {
                    vobjects = new ArrayList<EOEnterpriseObject>(1);
                    valueToObjects.put(v, vobjects);
                }
                vobjects.add(baseObject);

                /* Note: we could also use vobjects.keySet() */
                if (!srcValues.contains(v))
                    srcValues.add(v);
            }
        }
    }
}