com.healthmarketscience.jackcess.IndexCursor.java Source code

Java tutorial

Introduction

Here is the source code for com.healthmarketscience.jackcess.IndexCursor.java

Source

/*
Copyright (c) 2011 James Ahlborn
    
This library 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.1 of the License, or (at your option) any later version.
    
This library 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 this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
USA
*/

package com.healthmarketscience.jackcess;

import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import com.healthmarketscience.jackcess.Table.RowState;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Cursor backed by an index with extended traversal options.
 *
 * @author James Ahlborn
 */
public class IndexCursor extends Cursor {
    private static final Log LOG = LogFactory.getLog(IndexCursor.class);

    /** IndexDirHandler for forward traversal */
    private final IndexDirHandler _forwardDirHandler = new ForwardIndexDirHandler();
    /** IndexDirHandler for backward traversal */
    private final IndexDirHandler _reverseDirHandler = new ReverseIndexDirHandler();
    /** logical index which this cursor is using */
    private final Index _index;
    /** Cursor over the entries of the relevant index */
    private final IndexData.EntryCursor _entryCursor;
    /** column names for the index entry columns */
    private Set<String> _indexEntryPattern;

    private IndexCursor(Table table, Index index, IndexData.EntryCursor entryCursor) throws IOException {
        super(new Id(table, index), table, new IndexPosition(entryCursor.getFirstEntry()),
                new IndexPosition(entryCursor.getLastEntry()));
        _index = index;
        _index.initialize();
        _entryCursor = entryCursor;
    }

    /**
     * Creates an indexed cursor for the given table.
     * <p>
     * Note, index based table traversal may not include all rows, as certain
     * types of indexes do not include all entries (namely, some indexes ignore
     * null entries, see {@link Index#shouldIgnoreNulls}).
     * 
     * @param table the table over which this cursor will traverse
     * @param index index for the table which will define traversal order as
     *              well as enhance certain lookups
     */
    public static IndexCursor createCursor(Table table, Index index) throws IOException {
        return createCursor(table, index, null, null);
    }

    /**
     * Creates an indexed cursor for the given table, narrowed to the given
     * range.
     * <p>
     * Note, index based table traversal may not include all rows, as certain
     * types of indexes do not include all entries (namely, some indexes ignore
     * null entries, see {@link Index#shouldIgnoreNulls}).
     * 
     * @param table the table over which this cursor will traverse
     * @param index index for the table which will define traversal order as
     *              well as enhance certain lookups
     * @param startRow the first row of data for the cursor (inclusive), or
     *                 {@code null} for the first entry
     * @param endRow the last row of data for the cursor (inclusive), or
     *               {@code null} for the last entry
     */
    public static IndexCursor createCursor(Table table, Index index, Object[] startRow, Object[] endRow)
            throws IOException {
        return createCursor(table, index, startRow, true, endRow, true);
    }

    /**
     * Creates an indexed cursor for the given table, narrowed to the given
     * range.
     * <p>
     * Note, index based table traversal may not include all rows, as certain
     * types of indexes do not include all entries (namely, some indexes ignore
     * null entries, see {@link Index#shouldIgnoreNulls}).
     * 
     * @param table the table over which this cursor will traverse
     * @param index index for the table which will define traversal order as
     *              well as enhance certain lookups
     * @param startRow the first row of data for the cursor, or {@code null} for
     *                 the first entry
     * @param startInclusive whether or not startRow is inclusive or exclusive
     * @param endRow the last row of data for the cursor, or {@code null} for
     *               the last entry
     * @param endInclusive whether or not endRow is inclusive or exclusive
     */
    public static IndexCursor createCursor(Table table, Index index, Object[] startRow, boolean startInclusive,
            Object[] endRow, boolean endInclusive) throws IOException {
        if (table != index.getTable()) {
            throw new IllegalArgumentException("Given index is not for given table: " + index + ", " + table);
        }
        if (!table.getFormat().INDEXES_SUPPORTED) {
            throw new IllegalArgumentException(
                    "JetFormat " + table.getFormat() + " does not currently support index lookups");
        }
        if (index.getIndexData().isReadOnly()) {
            throw new IllegalArgumentException(
                    "Given index " + index + " is not usable for indexed lookups because it is read-only");
        }
        IndexCursor cursor = new IndexCursor(table, index,
                index.cursor(startRow, startInclusive, endRow, endInclusive));
        // init the column matcher appropriately for the index type
        cursor.setColumnMatcher(null);
        return cursor;
    }

    public Index getIndex() {
        return _index;
    }

    /**
     * @deprecated renamed to {@link #findFirstRowByEntry(Object...)} to be more
     * clear
     */
    @Deprecated
    public boolean findRowByEntry(Object... entryValues) throws IOException {
        return findFirstRowByEntry(entryValues);
    }

    /**
     * Moves to the first row (as defined by the cursor) where the index entries
     * match the given values.  If a match is not found (or an exception is
     * thrown), the cursor is restored to its previous state.
     * <p>
     * Warning, this method <i>always</i> starts searching from the beginning of
     * the Table (you cannot use it to find successive matches).
     *
     * @param entryValues the column values for the index's columns.
     * @return {@code true} if a valid row was found with the given values,
     *         {@code false} if no row was found
     */
    public boolean findFirstRowByEntry(Object... entryValues) throws IOException {
        Position curPos = _curPos;
        Position prevPos = _prevPos;
        boolean found = false;
        try {
            found = findFirstRowByEntryImpl(toRowValues(entryValues), true);
            return found;
        } finally {
            if (!found) {
                try {
                    restorePosition(curPos, prevPos);
                } catch (IOException e) {
                    LOG.error("Failed restoring position", e);
                }
            }
        }
    }

    /**
     * Moves to the first row (as defined by the cursor) where the index entries
     * are >= the given values.  If a an exception is thrown, the cursor is
     * restored to its previous state.
     *
     * @param entryValues the column values for the index's columns.
     */
    public void findClosestRowByEntry(Object... entryValues) throws IOException {
        Position curPos = _curPos;
        Position prevPos = _prevPos;
        boolean found = false;
        try {
            findFirstRowByEntryImpl(toRowValues(entryValues), false);
            found = true;
        } finally {
            if (!found) {
                try {
                    restorePosition(curPos, prevPos);
                } catch (IOException e) {
                    LOG.error("Failed restoring position", e);
                }
            }
        }
    }

    /**
     * Returns {@code true} if the current row matches the given index entries.
     * 
     * @param entryValues the column values for the index's columns.
     */
    public boolean currentRowMatchesEntry(Object... entryValues) throws IOException {
        return currentRowMatchesEntryImpl(toRowValues(entryValues));
    }

    /**
     * Returns a modifiable Iterator which will iterate through all the rows of
     * this table which match the given index entries.
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterator<Map<String, Object>> entryIterator(Object... entryValues) {
        return entryIterator((Collection<String>) null, entryValues);
    }

    /**
     * Returns a modifiable Iterator which will iterate through all the rows of
     * this table which match the given index entries, returning only the given
     * columns.
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterator<Map<String, Object>> entryIterator(Collection<String> columnNames, Object... entryValues) {
        return new EntryIterator(columnNames, toRowValues(entryValues));
    }

    /**
     * Returns an Iterable whose iterator() method returns the result of a call
     * to {@link #entryIterator(Object...)}
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> entryIterable(Object... entryValues) {
        return entryIterable((Collection<String>) null, entryValues);
    }

    /**
     * Returns an Iterable whose iterator() method returns the result of a call
     * to {@link #entryIterator(Collection,Object...)}
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> entryIterable(final Collection<String> columnNames,
            final Object... entryValues) {
        return new Iterable<Map<String, Object>>() {
            public Iterator<Map<String, Object>> iterator() {
                return new EntryIterator(columnNames, toRowValues(entryValues));
            }
        };
    }

    @Override
    protected IndexDirHandler getDirHandler(boolean moveForward) {
        return (moveForward ? _forwardDirHandler : _reverseDirHandler);
    }

    @Override
    protected boolean isUpToDate() {
        return (super.isUpToDate() && _entryCursor.isUpToDate());
    }

    @Override
    protected void reset(boolean moveForward) {
        _entryCursor.reset(moveForward);
        super.reset(moveForward);
    }

    @Override
    protected void restorePositionImpl(Position curPos, Position prevPos) throws IOException {
        if (!(curPos instanceof IndexPosition) || !(prevPos instanceof IndexPosition)) {
            throw new IllegalArgumentException("Restored positions must be index positions");
        }
        _entryCursor.restorePosition(((IndexPosition) curPos).getEntry(), ((IndexPosition) prevPos).getEntry());
        super.restorePositionImpl(curPos, prevPos);
    }

    @Override
    protected boolean findNextRowImpl(Column columnPattern, Object valuePattern) throws IOException {
        if (!isBeforeFirst()) {
            // use the default table scan for finding rows mid-cursor
            return super.findNextRowImpl(columnPattern, valuePattern);
        }

        // searching for the first match
        Object[] rowValues = _entryCursor.getIndexData().constructIndexRow(columnPattern.getName(), valuePattern);

        if (rowValues == null) {
            // bummer, use the default table scan
            return super.findNextRowImpl(columnPattern, valuePattern);
        }

        // sweet, we can use our index
        if (!findPotentialRow(rowValues, true)) {
            return false;
        }

        // either we found a row with the given value, or none exist in the
        // table
        return currentRowMatches(columnPattern, valuePattern);
    }

    /**
     * Moves to the first row (as defined by the cursor) where the index entries
     * match the given values.  Caller manages save/restore on failure.
     *
     * @param rowValues the column values built from the index column values
     * @param requireMatch whether or not an exact match is found
     * @return {@code true} if a valid row was found with the given values,
     *         {@code false} if no row was found
     */
    protected boolean findFirstRowByEntryImpl(Object[] rowValues, boolean requireMatch) throws IOException {
        if (!findPotentialRow(rowValues, requireMatch)) {
            return false;
        } else if (!requireMatch) {
            // nothing more to do, we have moved to the closest row
            return true;
        }

        return currentRowMatchesEntryImpl(rowValues);
    }

    @Override
    protected boolean findNextRowImpl(Map<String, ?> rowPattern) throws IOException {
        if (!isBeforeFirst()) {
            // use the default table scan for finding rows mid-cursor
            return super.findNextRowImpl(rowPattern);
        }

        // searching for the first match
        IndexData indexData = _entryCursor.getIndexData();
        Object[] rowValues = indexData.constructIndexRow(rowPattern);

        if (rowValues == null) {
            // bummer, use the default table scan
            return super.findNextRowImpl(rowPattern);
        }

        // sweet, we can use our index
        if (!findPotentialRow(rowValues, true)) {
            // at end of index, no potential matches
            return false;
        }

        // find actual matching row
        Map<String, ?> indexRowPattern = null;
        if (rowPattern.size() == indexData.getColumns().size()) {
            // the rowPattern matches our index columns exactly, so we can
            // streamline our testing below
            indexRowPattern = rowPattern;
        } else {
            // the rowPattern has more columns than just the index, so we need to
            // do more work when testing below
            Map<String, Object> tmpRowPattern = new LinkedHashMap<String, Object>();
            indexRowPattern = tmpRowPattern;
            for (IndexData.ColumnDescriptor idxCol : indexData.getColumns()) {
                tmpRowPattern.put(idxCol.getName(), rowValues[idxCol.getColumnIndex()]);
            }
        }

        // there may be multiple columns which fit the pattern subset used by
        // the index, so we need to keep checking until our index values no
        // longer match
        do {

            if (!currentRowMatches(indexRowPattern)) {
                // there are no more rows which could possibly match
                break;
            }

            // note, if rowPattern == indexRowPattern, no need to do an extra
            // comparison with the current row
            if ((rowPattern == indexRowPattern) || currentRowMatches(rowPattern)) {
                // found it!
                return true;
            }

        } while (moveToNextRow());

        // none of the potential rows matched
        return false;
    }

    private boolean currentRowMatchesEntryImpl(Object[] rowValues) throws IOException {
        if (_indexEntryPattern == null) {
            // init our set of index column names
            _indexEntryPattern = new HashSet<String>();
            for (IndexData.ColumnDescriptor col : getIndex().getColumns()) {
                _indexEntryPattern.add(col.getName());
            }
        }

        // check the next row to see if it actually matches
        Map<String, Object> row = getCurrentRow(_indexEntryPattern);

        for (IndexData.ColumnDescriptor col : getIndex().getColumns()) {
            String columnName = col.getName();
            Object patValue = rowValues[col.getColumnIndex()];
            Object rowValue = row.get(columnName);
            if (!_columnMatcher.matches(getTable(), columnName, patValue, rowValue)) {
                return false;
            }
        }

        return true;
    }

    private boolean findPotentialRow(Object[] rowValues, boolean requireMatch) throws IOException {
        _entryCursor.beforeEntry(rowValues);
        IndexData.Entry startEntry = _entryCursor.getNextEntry();
        if (requireMatch && !startEntry.getRowId().isValid()) {
            // at end of index, no potential matches
            return false;
        }
        // move to position and check it out
        restorePosition(new IndexPosition(startEntry));
        return true;
    }

    private Object[] toRowValues(Object[] entryValues) {
        return _entryCursor.getIndexData().constructIndexRowFromEntry(entryValues);
    }

    @Override
    protected Position findAnotherPosition(RowState rowState, Position curPos, boolean moveForward)
            throws IOException {
        IndexDirHandler handler = getDirHandler(moveForward);
        IndexPosition endPos = (IndexPosition) handler.getEndPosition();
        IndexData.Entry entry = handler.getAnotherEntry();
        return ((!entry.equals(endPos.getEntry())) ? new IndexPosition(entry) : endPos);
    }

    @Override
    protected ColumnMatcher getDefaultColumnMatcher() {
        if (getIndex().isUnique()) {
            // text indexes are case-insensitive, therefore we should always use a
            // case-insensitive matcher for unique indexes.
            return CaseInsensitiveColumnMatcher.INSTANCE;
        }
        return SimpleColumnMatcher.INSTANCE;
    }

    /**
     * Handles moving the table index cursor in a given direction.  Separates
     * cursor logic from value storage.
     */
    private abstract class IndexDirHandler extends DirHandler {
        public abstract IndexData.Entry getAnotherEntry() throws IOException;
    }

    /**
     * Handles moving the table index cursor forward.
     */
    private final class ForwardIndexDirHandler extends IndexDirHandler {
        @Override
        public Position getBeginningPosition() {
            return getFirstPosition();
        }

        @Override
        public Position getEndPosition() {
            return getLastPosition();
        }

        @Override
        public IndexData.Entry getAnotherEntry() throws IOException {
            return _entryCursor.getNextEntry();
        }
    }

    /**
     * Handles moving the table index cursor backward.
     */
    private final class ReverseIndexDirHandler extends IndexDirHandler {
        @Override
        public Position getBeginningPosition() {
            return getLastPosition();
        }

        @Override
        public Position getEndPosition() {
            return getFirstPosition();
        }

        @Override
        public IndexData.Entry getAnotherEntry() throws IOException {
            return _entryCursor.getPreviousEntry();
        }
    }

    /**
     * Value object which maintains the current position of an IndexCursor.
     */
    private static final class IndexPosition extends Position {
        private final IndexData.Entry _entry;

        private IndexPosition(IndexData.Entry entry) {
            _entry = entry;
        }

        @Override
        public RowId getRowId() {
            return getEntry().getRowId();
        }

        public IndexData.Entry getEntry() {
            return _entry;
        }

        @Override
        protected boolean equalsImpl(Object o) {
            return getEntry().equals(((IndexPosition) o).getEntry());
        }

        @Override
        public String toString() {
            return "Entry = " + getEntry();
        }
    }

    /**
     * Row iterator (by matching entry) for this cursor, modifiable.
     */
    private final class EntryIterator extends BaseIterator {
        private final Object[] _rowValues;

        private EntryIterator(Collection<String> columnNames, Object[] rowValues) {
            super(columnNames);
            _rowValues = rowValues;
            try {
                _hasNext = findFirstRowByEntryImpl(rowValues, true);
                _validRow = _hasNext;
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
        }

        @Override
        protected boolean findNext() throws IOException {
            return (moveToNextRow() && currentRowMatchesEntryImpl(_rowValues));
        }
    }

}