/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 2000-2003
 *      Sleepycat Software.  All rights reserved.
 *
 * $Id: StoredContainer.java,v 1.17 2003/10/22 14:28:39 mhayes Exp $
 */

package com.sleepycat.bdb.collection;

import com.sleepycat.db.Db;
import com.sleepycat.db.DbEnv;
import com.sleepycat.db.DbException;
import com.sleepycat.bdb.CurrentTransaction;
import com.sleepycat.bdb.DataCursor;
import com.sleepycat.bdb.DataIndex;
import com.sleepycat.bdb.DataStore;
import com.sleepycat.bdb.DataView;
import com.sleepycat.bdb.util.RuntimeExceptionWrapper;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

/**
 * A abstract base class for all stored collections and maps.  This class
 * provides implementations of methods that are common to the {@link
 * Collection} and the {@link Map} interfaces, namely {@link #clear}, {@link
 * #isEmpty} and {@link #size}.
 *
 * <p>In addition, this class provides the following methods for stored
 * collections only.  Note that the use of these methods is not compatible with
 * the standard Java collections interface.</p>
 * <ul>
 * <li>{@link #isWriteAllowed()}</li>
 * <li>{@link #isIndexed()}</li>
 * <li>{@link #isOrdered()}</li>
 * <li>{@link #areDuplicatesAllowed()}</li>
 * <li>{@link #areDuplicatesOrdered()}</li>
 * <li>{@link #areKeysRenumbered()}</li>
 * <li>{@link #isDirtyReadAllowed()}</li>
 * <li>{@link #isDirtyReadEnabled()}</li>
 * <li>{@link #isAutoCommit()}</li>
 * <li>{@link #isTransactional()}</li>
 * </ul>
 *
 * @author Mark Hayes
 */
public abstract class StoredContainer implements Cloneable {

    DataView view;

    StoredContainer(DataView view) {

        this.view = view;
    }

    /**
     * Returns true if this is a read-write container or false if this is a
     * read-only container.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether write is allowed.
     */
    public final boolean isWriteAllowed() {

        return view.isWriteAllowed();
    }

    /**
     * Returns whether dirty-read is allowed for this container.
     * Dirty-read is allowed if Db.DB_DIRTY_READ was specified when opening the
     * database of the underlying Store and Index (if any) for this container.
     * Even when dirty-read is allowed it must specifically be enabled by
     * calling one of the {@link StoredCollections} methods.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether dirty-read is allowed.
     */
    public final boolean isDirtyReadAllowed() {

        return view.isDirtyReadAllowed();
    }

    /**
     * Returns whether dirty-read is enabled for this container.
     * If dirty-read is enabled, data will be read that is modified but not
     * committed.
     * Dirty-read is disabled by default.
     * This method always returns false if {@link #isDirtyReadAllowed} returns
     * false.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether dirty-read is enabled.
     */
    public final boolean isDirtyReadEnabled() {

        return view.isDirtyReadEnabled();
    }

    /**
     * Returns whether auto-commit is enabled for this container or for its
     * associated {@link DbEnv}.
     * If auto-commit is enabled, a transaction will be started and committed
     * automatically for each write operation when no transaction is active.
     * Auto-commit only applies to container methods.  It does not apply to
     * iterator methods and these always require an explict transaction.
     * Auto-commit is disabled by default for a container but may be enabled
     * for an entire environment using {@link DbEnv#setFlags}.
     * This method always returns false if {@link #isTransactional} returns
     * false.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether auto-commit is enabled.
     */
    public final boolean isAutoCommit() {

        return view.isAutoCommit();
    }

    /**
     * Returns whether the databases underlying this container are
     * transactional.
     * Even in a transactional environment, a database will be transactional
     * only if it was opened within a transaction or if the auto-commit option
     * was specified when it was opened.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether the database is transactional.
     */
    public final boolean isTransactional() {

        return view.isTransactional();
    }

    final StoredContainer dirtyReadClone() {

        if (!isDirtyReadAllowed())
            return this;
        try {
            StoredContainer cont = (StoredContainer) clone();
            cont.view = cont.view.dirtyReadView(true);
            return cont;
        } catch (CloneNotSupportedException willNeverOccur) { return null; }
    }

    final StoredContainer autoCommitClone() {

        if (!isTransactional())
            return this;
        try {
            StoredContainer cont = (StoredContainer) clone();
            cont.view = cont.view.autoCommitView(true);
            return cont;
        } catch (CloneNotSupportedException willNeverOccur) { return null; }
    }

    /**
     * Returns whether duplicate keys are allowed in this container.
     * Duplicates are optionally allowed for HASH and BTREE databases.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether duplicates are allowed.
     */
    public final boolean areDuplicatesAllowed() {

        return view.areDuplicatesAllowed();
    }

    /**
     * Returns whether duplicate keys are allowed and sorted by element value.
     * Duplicates are optionally sorted for HASH and BTREE databases.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether duplicates are ordered.
     */
    public final boolean areDuplicatesOrdered() {

        return view.areDuplicatesOrdered();
    }

    /**
     * Returns whether keys are renumbered when insertions and deletions occur.
     * Keys are optionally renumbered for RECNO databases.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether keys are renumbered.
     */
    public final boolean areKeysRenumbered() {

        return view.areKeysRenumbered();
    }

    /**
     * Returns whether keys are ordered in this container.
     * Keys are ordered for BTREE, RECNO and QUEUE database.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether keys are ordered.
     */
    public final boolean isOrdered() {

        return view.isOrdered();
    }

    /**
     * Returns whether this container is a view on a {@link DataIndex} rather
     * than directly on a {@link DataStore}.
     * This method does not exist in the standard {@link Map} or {@link
     * Collection} interfaces.
     *
     * @return whether the view is indexed.
     */
    public final boolean isIndexed() {

        return (view.getIndex() != null);
    }

    /**
     * Always throws UnsupportedOperationException.  The size of a database
     * cannot be obtained reliably or inexpensively.
     * This method therefore violates the {@link Collection#size} and {@link
     * Map#size} interfaces.
     *
     * @return always throws an exception.
     *
     * @throws UnsupportedOperationException unconditionally.
     */
    public int size() {

        throw new UnsupportedOperationException(
            "collection size not available");
    }

    /**
     * Returns true if this map or collection contains no mappings or elements.
     * This method conforms to the {@link Collection#isEmpty} and {@link
     * Map#isEmpty} interfaces.
     *
     * @return whether the container is empty.
     *
     * @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
     */
    public boolean isEmpty() {

        try {
            return view.isEmpty();
        } catch (Exception e) {
            throw StoredContainer.convertException(e);
        }
    }

    /**
     * Removes all mappings or elements from this map or collection (optional
     * operation).
     * This method conforms to the {@link Collection#clear} and {@link
     * Map#clear} interfaces.
     *
     * @throws UnsupportedOperationException if the container is read-only.
     *
     * @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
     */
    public void clear() {

        boolean doAutoCommit = beginAutoCommit();
        try {
            view.clear(null);
            commitAutoCommit(doAutoCommit);
        } catch (Exception e) {
            throw handleException(e, doAutoCommit);
        }
    }

    Object get(Object key) {

        try {
            Object[] ret = new Object[1];
            int err = view.get(key, null, Db.DB_SET, false, ret);
            return (err == 0) ? ret[0] : null;
        } catch (Exception e) {
            throw StoredContainer.convertException(e);
        }
    }

    Object put(final Object key, final Object value) {

        boolean doAutoCommit = beginAutoCommit();
        try {
            Object[] oldValue = new Object[1];
            view.put(key, value, 0, oldValue);
            commitAutoCommit(doAutoCommit);
            return oldValue[0];
        } catch (Exception e) {
            throw handleException(e, doAutoCommit);
        }
    }

    final boolean removeKey(final Object key, final Object[] oldVal) {

        DataCursor cursor = null;
        boolean doAutoCommit = beginAutoCommit();
        try {
            cursor = new DataCursor(view, true);
            boolean found = false;
            int err = cursor.get(key, null, Db.DB_SET, true);
            while (err == 0) {
                cursor.delete();
                found = true;
                if (oldVal != null && oldVal[0] == null)
                    oldVal[0] = cursor.getCurrentValue();
                err = cursor.get(null, null, Db.DB_NEXT_DUP, true);
            }
            closeCursor(cursor);
            commitAutoCommit(doAutoCommit);
            return found;
        } catch (Exception e) {
            closeCursor(cursor);
            throw handleException(e, doAutoCommit);
        }
    }

    boolean containsKey(Object key) {

        DataCursor cursor = null;
        try {
            cursor = new DataCursor(view, false);
            int err = cursor.get(key, null, Db.DB_SET, false);
            return (err == 0);
        } catch (Exception e) {
            throw StoredContainer.convertException(e);
        } finally {
            closeCursor(cursor);
        }
    }

    final boolean removeValue(Object value) {

        DataCursor cursor = null;
        boolean doAutoCommit = beginAutoCommit();
        try {
            cursor = new DataCursor(view, true);
            int err = cursor.find(value, true);
            if (err == 0) {
                cursor.delete();
            }
            closeCursor(cursor);
            commitAutoCommit(doAutoCommit);
            return (err == 0);
        } catch (Exception e) {
            closeCursor(cursor);
            throw handleException(e, doAutoCommit);
        }
    }

    boolean containsValue(Object value) {

        DataCursor cursor = null;
        try {
            cursor = new DataCursor(view, false);
            int err = cursor.find(value, true);
            return (err == 0);
        } catch (Exception e) {
            throw StoredContainer.convertException(e);
        } finally {
            closeCursor(cursor);
        }
    }

    final void closeCursor(DataCursor cursor) {

        if (cursor != null) {
            try {
                cursor.close();
            } catch (Exception e) {
                throw StoredContainer.convertException(e);
            }
        }
    }

    final boolean beginAutoCommit() {

        if (isAutoCommit()) {
            try {
                CurrentTransaction currentTxn = view.getCurrentTxn();
                if (currentTxn.getTxn() == null) {
                    currentTxn.beginTxn();
                    return true;
                }
            } catch (DbException e) {
                throw new RuntimeExceptionWrapper(e);
            }
        }
        return false;
    }

    final void commitAutoCommit(boolean doAutoCommit)
        throws DbException {

        if (doAutoCommit) view.getCurrentTxn().commitTxn();
    }

    final RuntimeException handleException(Exception e, boolean doAutoCommit) {

        if (doAutoCommit) {
            try {
                view.getCurrentTxn().abortTxn();
            } catch (DbException ignored) {
            }
        }
        return StoredContainer.convertException(e);
    }

    static RuntimeException convertException(Exception e) {

        if (e instanceof RuntimeException) {
            return (RuntimeException) e;
        } else {
            return new RuntimeExceptionWrapper(e);
        }
    }
}
