/* * Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates. * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks * of Amazon Technologies, Inc. or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * 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.amazon.carbonado.raw; import java.util.NoSuchElementException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import com.amazon.carbonado.FetchException; import com.amazon.carbonado.cursor.AbstractCursor; /** * Abstract Cursor implementation for a repository that manipulates raw bytes. * * @author Brian S O'Neill */ public abstract class RawCursor<S> extends AbstractCursor<S> { // States for mState. private static final byte UNINITIALIZED = 0, CLOSED = 1, TRY_NEXT = 2, HAS_NEXT = 3; /** Lock object, as passed into the constructor */ protected final Lock mLock; private final byte[] mStartBound; private final boolean mInclusiveStart; private final byte[] mEndBound; private final boolean mInclusiveEnd; private final int mPrefixLength; private final boolean mReverse; private byte mState; /** * @param lock operations lock on this object * @param startBound specify the starting key for the cursor, or null if first * @param inclusiveStart true if start bound is inclusive * @param endBound specify the ending key for the cursor, or null if last * @param inclusiveEnd true if end bound is inclusive * @param maxPrefix maximum expected common initial bytes in start and end bound * @param reverse when true, iteration is reversed * @throws IllegalArgumentException if any bound is null but is not inclusive */ protected RawCursor(Lock lock, byte[] startBound, boolean inclusiveStart, byte[] endBound, boolean inclusiveEnd, int maxPrefix, boolean reverse) { mLock = lock == null ? new ReentrantLock() : lock; if ((startBound == null && !inclusiveStart) || (endBound == null && !inclusiveEnd)) { throw new IllegalArgumentException(); } mStartBound = startBound; mInclusiveStart = inclusiveStart; mEndBound = endBound; mInclusiveEnd = inclusiveEnd; mReverse = reverse; // Determine common prefix for start and end bound. if (maxPrefix <= 0 || startBound == null && endBound == null) { mPrefixLength = 0; } else { int len = Math.min(maxPrefix, Math.min(startBound == null ? 0 : startBound.length, endBound == null ? 0 : endBound.length)); int i; for (i=0; i<len; i++) { if (startBound[i] != endBound[i]) { break; } } mPrefixLength = i; } } public void close() throws FetchException { mLock.lock(); try { if (mState != CLOSED) { release(); // Switch state to closed before committing transaction, to // prevent infinite recursion that results when transaction // exits. Exiting a transaction causes all cursors to close. mState = CLOSED; } } finally { mLock.unlock(); } } public boolean hasNext() throws FetchException { try { mLock.lock(); try { switch (mState) { case UNINITIALIZED: if (mReverse ? toBoundedLast() : toBoundedFirst()) { mState = HAS_NEXT; return true; } else { mState = TRY_NEXT; } break; case CLOSED: default: return false; case TRY_NEXT: if (mReverse ? toBoundedPrevious() : toBoundedNext()) { mState = HAS_NEXT; return true; } break; case HAS_NEXT: return true; } } finally { mLock.unlock(); } } catch (FetchException e) { // Auto-close in response to FetchException. try { close(); } catch (FetchException e2) { // Ignore. } throw e; } // Reached the end naturally, so close. close(); return false; } public S next() throws FetchException, NoSuchElementException { try { mLock.lock(); try { if (!hasNext()) { handleNoSuchElement(); throw new NoSuchElementException(); } S obj = instantiateCurrent(); mState = TRY_NEXT; return obj; } finally { mLock.unlock(); } } catch (FetchException e) { // Auto-close in response to FetchException. try { close(); } catch (FetchException e2) { // Ignore. } throw e; } } @Override public int skipNext(int amount) throws FetchException { if (amount <= 0) { if (amount < 0) { throw new IllegalArgumentException("Cannot skip negative amount: " + amount); } return 0; } try { int actual = 0; mLock.lock(); try { if (hasNext()) { actual += mReverse ? toBoundedPrevious(amount) : toBoundedNext(amount); if (actual >= amount) { return actual; } mState = TRY_NEXT; // Since state was HAS_NEXT and is forced into TRY_NEXT, actual // amount skipped is effectively one more. actual++; } } finally { mLock.unlock(); } // Reached the end naturally, so close. close(); return actual; } catch (FetchException e) { // Auto-close in response to FetchException. try { close(); } catch (FetchException e2) { // Ignore. } throw e; } } /** * Release any internal resources, called when closed. */ protected abstract void release() throws FetchException; /** * Returns the contents of the current key being referenced, or null * otherwise. Caller is responsible for making a copy of the key. The array * must not be modified concurrently. * * <p>If cursor is not opened, null must be returned. * * @return currently referenced key bytes or null if no current * @throws IllegalStateException if key is disabled */ protected abstract byte[] getCurrentKey() throws FetchException; /** * Returns the contents of the current value being referenced, or null * otherwise. Caller is responsible for making a copy of the value. The * array must not be modified concurrently. * * <p>If cursor is not opened, null must be returned. * * @return currently referenced value bytes or null if no current * @throws IllegalStateException if value is disabled */ protected abstract byte[] getCurrentValue() throws FetchException; /** * An optimization hint which disables key and value acquisition. The * default implementation of this method does nothing. */ protected void disableKeyAndValue() { } /** * An optimization hint which disables just value acquisition. The default * implementation of this method does nothing. */ protected void disableValue() { } /** * Enable key and value acquisition again, after they have been * disabled. Calling this method forces the key and value to be * re-acquired, if they had been disabled. Key and value acquisition must * be enabled by default. The default implementation of this method does * nothing. */ protected void enableKeyAndValue() throws FetchException { } /** * Returns a new Storable instance for the currently referenced entry. * * @return new Storable instance, never null * @throws IllegalStateException if no current entry to instantiate */ protected abstract S instantiateCurrent() throws FetchException; /** * Move the cursor to the first available entry. If false is returned, the * cursor must be positioned before the first available entry. * * @return true if first entry exists and is now current * @throws IllegalStateException if cursor is not opened */ protected abstract boolean toFirst() throws FetchException; /** * Move the cursor to the first available entry at or after the given * key. If false is returned, the cursor must be positioned before the * first available entry. Caller is responsible for preserving contents of * array. * * @param key key to search for * @return true if first entry exists and is now current * @throws IllegalStateException if cursor is not opened */ protected abstract boolean toFirst(byte[] key) throws FetchException; /** * Move the cursor to the last available entry. If false is returned, the * cursor must be positioned after the last available entry. * * @return true if last entry exists and is now current * @throws IllegalStateException if cursor is not opened */ protected abstract boolean toLast() throws FetchException; /** * Move the cursor to the last available entry at or before the given * key. If false is returned, the cursor must be positioned after the last * available entry. Caller is responsible for preserving contents of array. * * @param key key to search for * @return true if last entry exists and is now current * @throws IllegalStateException if cursor is not opened */ protected abstract boolean toLast(byte[] key) throws FetchException; /** * Move the cursor to the next available entry, returning false if none. If * false is returned, the cursor must be positioned after the last * available entry. * * @return true if moved to next entry * @throws IllegalStateException if cursor is not opened */ protected abstract boolean toNext() throws FetchException; /** * Move the cursor to the next available entry, incrementing by the amount * given. The actual amount incremented is returned. If the amount is less * then requested, the cursor must be positioned after the last available * entry. Subclasses may wish to override this method with a faster * implementation. * * <p>Calling to toNext(1) is equivalent to calling toNext(). * * @param amount positive amount to advance * @return actual amount advanced * @throws IllegalStateException if cursor is not opened */ protected int toNext(int amount) throws FetchException { if (amount <= 1) { return (amount <= 0) ? 0 : (toNext() ? 1 : 0); } int count = 0; disableKeyAndValue(); try { while (amount > 0) { if (toNext()) { count++; amount--; } else { break; } } } finally { enableKeyAndValue(); } return count; } /** * Move the cursor to the next unique key, returning false if none. If * false is returned, the cursor must be positioned after the last * available entry. Subclasses may wish to override this method with a * faster implementation. * * @return true if moved to next unique key * @throws IllegalStateException if cursor is not opened */ protected boolean toNextKey() throws FetchException { byte[] initialKey = getCurrentKey(); if (initialKey == null) { return false; } disableValue(); try { while (true) { if (toNext()) { byte[] currentKey = getCurrentKey(); if (currentKey == null) { return false; } if (compareKeysPartially(currentKey, initialKey) > 0) { break; } } else { return false; } } } finally { enableKeyAndValue(); } return true; } /** * Move the cursor to the previous available entry, returning false if * none. If false is returned, the cursor must be positioned before the * first available entry. * * @return true if moved to previous entry * @throws IllegalStateException if cursor is not opened */ protected abstract boolean toPrevious() throws FetchException; /** * Move the cursor to the previous available entry, decrementing by the * amount given. The actual amount decremented is returned. If the amount * is less then requested, the cursor must be positioned before the first * available entry. Subclasses may wish to override this method with a * faster implementation. * * <p>Calling to toPrevious(1) is equivalent to calling toPrevious(). * * @param amount positive amount to retreat * @return actual amount retreated * @throws IllegalStateException if cursor is not opened */ protected int toPrevious(int amount) throws FetchException { if (amount <= 1) { return (amount <= 0) ? 0 : (toPrevious() ? 1 : 0); } int count = 0; disableKeyAndValue(); try { while (amount > 0) { if (toPrevious()) { count++; amount--; } else { break; } } } finally { enableKeyAndValue(); } return count; } /** * Move the cursor to the previous unique key, returning false if none. If * false is returned, the cursor must be positioned before the first * available entry. Subclasses may wish to override this method with a * faster implementation. * * @return true if moved to previous unique key * @throws IllegalStateException if cursor is not opened */ protected boolean toPreviousKey() throws FetchException { byte[] initialKey = getCurrentKey(); if (initialKey == null) { return false; } disableValue(); try { while (true) { if (toPrevious()) { byte[] currentKey = getCurrentKey(); if (currentKey == null) { return false; } if (compareKeysPartially(getCurrentKey(), initialKey) < 0) { break; } } else { return false; } } } finally { enableKeyAndValue(); } return true; } /** * Returns {@literal <0} if key1 is less, 0 if equal (at least partially), * {@literal >0} if key1 is greater. */ protected int compareKeysPartially(byte[] key1, byte[] key2) { int length = Math.min(key1.length, key2.length); for (int i=0; i<length; i++) { int a1 = key1[i]; int a2 = key2[i]; if (a1 != a2) { return (a1 & 0xff) - (a2 & 0xff); } } return 0; } /** * Called right before throwing NoSuchElementException. Subclasses may * override to do special checks or throw a different exception. */ protected void handleNoSuchElement() throws FetchException { } private boolean prefixMatches() throws FetchException { int prefixLen = mPrefixLength; if (prefixLen > 0) { byte[] prefix = mStartBound; byte[] key = getCurrentKey(); if (key == null) { return false; } for (int i=0; i<prefixLen; i++) { if (prefix[i] != key[i]) { return false; } } } return true; } // Calls toFirst, but considers start and end bounds. private boolean toBoundedFirst() throws FetchException { if (mStartBound == null) { if (!toFirst()) { return false; } } else { if (!toFirst(mStartBound.clone())) { return false; } if (!mInclusiveStart) { byte[] currentKey = getCurrentKey(); if (currentKey == null) { return false; } if (compareKeysPartially(mStartBound, currentKey) == 0) { if (!toNextKey()) { return false; } } } } if (mEndBound != null) { byte[] currentKey = getCurrentKey(); if (currentKey == null) { return false; } int result = compareKeysPartially(currentKey, mEndBound); if (result >= 0) { if (result > 0 || !mInclusiveEnd) { return false; } } } return prefixMatches(); } // Calls toLast, but considers start and end bounds. Caller is responsible // for preserving key. private boolean toBoundedLast() throws FetchException { if (mEndBound == null) { if (!toLast()) { return false; } } else { if (!toLast(mEndBound.clone())) { return false; } if (!mInclusiveEnd) { byte[] currentKey = getCurrentKey(); if (currentKey == null) { return false; } if (compareKeysPartially(mEndBound, currentKey) == 0) { if (!toPreviousKey()) { return false; } } } } if (mStartBound != null) { byte[] currentKey = getCurrentKey(); if (currentKey == null) { return false; } int result = compareKeysPartially(currentKey, mStartBound); if (result <= 0) { if (result < 0 || !mInclusiveStart) { return false; } } } return prefixMatches(); } // Calls toNext, but considers end bound. private boolean toBoundedNext() throws FetchException { if (!toNext()) { return false; } if (mEndBound != null) { byte[] currentKey = getCurrentKey(); if (currentKey == null) { return false; } int result = compareKeysPartially(currentKey, mEndBound); if (result >= 0) { if (result > 0 || !mInclusiveEnd) { return false; } } } return prefixMatches(); } // Calls toNext, but considers end bound. private int toBoundedNext(int amount) throws FetchException { if (mEndBound == null) { return toNext(amount); } int count = 0; disableValue(); try { while (amount > 0) { if (!toNext()) { break; } byte[] currentKey = getCurrentKey(); if (currentKey == null) { break; } int result = compareKeysPartially(currentKey, mEndBound); if (result >= 0) { if (result > 0 || !mInclusiveEnd) { break; } } if (!prefixMatches()) { break; } count++; amount--; } } finally { enableKeyAndValue(); } return count; } // Calls toPrevious, but considers start bound. private boolean toBoundedPrevious() throws FetchException { if (!toPrevious()) { return false; } if (mStartBound != null) { byte[] currentKey = getCurrentKey(); if (currentKey == null) { return false; } int result = compareKeysPartially(currentKey, mStartBound); if (result <= 0) { if (result < 0 || !mInclusiveStart) { // Too far now, reset to first. toBoundedFirst(); return false; } } } return prefixMatches(); } // Calls toPrevious, but considers start bound. private int toBoundedPrevious(int amount) throws FetchException { if (mStartBound == null) { return toPrevious(amount); } int count = 0; disableValue(); try { while (amount > 0) { if (!toPrevious()) { break; } byte[] currentKey = getCurrentKey(); if (currentKey == null) { break; } int result = compareKeysPartially(currentKey, mStartBound); if (result <= 0) { if (result < 0 || !mInclusiveStart) { // Too far now, reset to first. toBoundedFirst(); break; } } if (!prefixMatches()) { break; } count++; amount--; } } finally { enableKeyAndValue(); } return count; } }