blob: b55333011f6bc9ba9be146c633ea08a5346b6cf8 [file] [log] [blame]
/*-
* Copyright (C) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle Berkeley
* DB Java Edition made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle Berkeley DB Java Edition for a copy of the
* license and additional information.
*/
package com.sleepycat.collections;
import com.sleepycat.compat.DbCompat;
import com.sleepycat.bind.EntityBinding;
import com.sleepycat.bind.EntryBinding;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.JoinConfig;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.SecondaryConfig;
import com.sleepycat.je.SecondaryDatabase;
import com.sleepycat.je.SecondaryKeyCreator;
import com.sleepycat.je.Transaction;
import com.sleepycat.util.RuntimeExceptionWrapper;
import com.sleepycat.util.keyrange.KeyRange;
import com.sleepycat.util.keyrange.KeyRangeException;
/**
* Represents a Berkeley DB database and adds support for indices, bindings and
* key ranges.
*
* <p>This class defines a view and takes care of reading and updating indices,
* calling bindings, constraining access to a key range, etc.</p>
*
* @author Mark Hayes
*/
final class DataView implements Cloneable {
Database db;
SecondaryDatabase secDb;
CurrentTransaction currentTxn;
KeyRange range;
EntryBinding keyBinding;
EntryBinding valueBinding;
EntityBinding entityBinding;
PrimaryKeyAssigner keyAssigner;
SecondaryKeyCreator secKeyCreator;
CursorConfig cursorConfig; // Used for all operations via this view
boolean writeAllowed; // Read-write view
boolean ordered; // Not a HASH Db
boolean keyRangesAllowed; // BTREE only
boolean recNumAllowed; // QUEUE, RECNO, or BTREE-RECNUM Db
boolean recNumAccess; // recNumAllowed && using a rec num binding
boolean btreeRecNumDb; // BTREE-RECNUM Db
boolean btreeRecNumAccess; // recNumAccess && BTREE-RECNUM Db
boolean recNumRenumber; // RECNO-RENUM Db
boolean keysRenumbered; // recNumRenumber || btreeRecNumAccess
boolean dupsAllowed; // Dups configured
boolean dupsOrdered; // Sorted dups configured
boolean transactional; // Db is transactional
boolean readUncommittedAllowed; // Read-uncommited is optional in DB-CORE
/*
* If duplicatesView is called, dupsView will be true and dupsKey will be
* the secondary key used as the "single key" range. dupRange will be set
* as the range of the primary key values if subRange is subsequently
* called, to further narrow the view.
*/
DatabaseEntry dupsKey;
boolean dupsView;
KeyRange dupsRange;
/**
* Creates a view for a given database and bindings. The initial key range
* of the view will be open.
*/
DataView(Database database, EntryBinding keyBinding,
EntryBinding valueBinding, EntityBinding entityBinding,
boolean writeAllowed, PrimaryKeyAssigner keyAssigner)
throws IllegalArgumentException {
if (database == null) {
throw new IllegalArgumentException("database is null");
}
db = database;
try {
currentTxn =
CurrentTransaction.getInstanceInternal(db.getEnvironment());
DatabaseConfig dbConfig;
if (db instanceof SecondaryDatabase) {
secDb = (SecondaryDatabase) database;
SecondaryConfig secConfig = secDb.getSecondaryConfig();
secKeyCreator = secConfig.getKeyCreator();
dbConfig = secConfig;
} else {
dbConfig = db.getConfig();
}
ordered = !DbCompat.isTypeHash(dbConfig);
keyRangesAllowed = DbCompat.isTypeBtree(dbConfig);
recNumAllowed = DbCompat.isTypeQueue(dbConfig) ||
DbCompat.isTypeRecno(dbConfig) ||
DbCompat.getBtreeRecordNumbers(dbConfig);
recNumRenumber = DbCompat.getRenumbering(dbConfig);
dupsAllowed = DbCompat.getSortedDuplicates(dbConfig) ||
DbCompat.getUnsortedDuplicates(dbConfig);
dupsOrdered = DbCompat.getSortedDuplicates(dbConfig);
transactional = currentTxn.isTxnMode() &&
dbConfig.getTransactional();
readUncommittedAllowed = DbCompat.getReadUncommitted(dbConfig);
btreeRecNumDb = recNumAllowed && DbCompat.isTypeBtree(dbConfig);
range = new KeyRange(dbConfig.getBtreeComparator());
} catch (DatabaseException e) {
throw RuntimeExceptionWrapper.wrapIfNeeded(e);
}
this.writeAllowed = writeAllowed;
this.keyBinding = keyBinding;
this.valueBinding = valueBinding;
this.entityBinding = entityBinding;
this.keyAssigner = keyAssigner;
cursorConfig = CursorConfig.DEFAULT;
if (valueBinding != null && entityBinding != null)
throw new IllegalArgumentException
("both valueBinding and entityBinding are non-null");
if (keyBinding instanceof com.sleepycat.bind.RecordNumberBinding) {
if (!recNumAllowed) {
throw new IllegalArgumentException
("RecordNumberBinding requires DB_BTREE/DB_RECNUM, " +
"DB_RECNO, or DB_QUEUE");
}
recNumAccess = true;
if (btreeRecNumDb) {
btreeRecNumAccess = true;
}
}
keysRenumbered = recNumRenumber || btreeRecNumAccess;
}
/**
* Clones the view.
*/
private DataView cloneView() {
try {
return (DataView) super.clone();
} catch (CloneNotSupportedException willNeverOccur) {
throw DbCompat.unexpectedState();
}
}
/**
* Return a new key-set view derived from this view by setting the
* entity and value binding to null.
*
* @return the derived view.
*/
DataView keySetView() {
if (keyBinding == null) {
throw new UnsupportedOperationException("Must have keyBinding");
}
DataView view = cloneView();
view.valueBinding = null;
view.entityBinding = null;
return view;
}
/**
* Return a new value-set view derived from this view by setting the
* key binding to null.
*
* @return the derived view.
*/
DataView valueSetView() {
if (valueBinding == null && entityBinding == null) {
throw new UnsupportedOperationException
("Must have valueBinding or entityBinding");
}
DataView view = cloneView();
view.keyBinding = null;
return view;
}
/**
* Return a new value-set view for single key range.
*
* @param singleKey the single key value.
*
* @return the derived view.
*
* @throws DatabaseException if a database problem occurs.
*
* @throws KeyRangeException if the specified range is not within the
* current range.
*/
DataView valueSetView(Object singleKey)
throws DatabaseException, KeyRangeException {
/*
* Must do subRange before valueSetView since the latter clears the
* key binding needed for the former.
*/
KeyRange singleKeyRange = subRange(range, singleKey);
DataView view = valueSetView();
view.range = singleKeyRange;
return view;
}
/**
* Return a new value-set view for key range, optionally changing
* the key binding.
*/
DataView subView(Object beginKey, boolean beginInclusive,
Object endKey, boolean endInclusive,
EntryBinding keyBinding)
throws DatabaseException, KeyRangeException {
DataView view = cloneView();
view.setRange(beginKey, beginInclusive, endKey, endInclusive);
if (keyBinding != null) view.keyBinding = keyBinding;
return view;
}
/**
* Return a new duplicates view for a given secondary key.
*/
DataView duplicatesView(Object secondaryKey,
EntryBinding primaryKeyBinding)
throws DatabaseException, KeyRangeException {
if (!isSecondary()) {
throw new UnsupportedOperationException
("Only allowed for maps on secondary databases");
}
if (dupsView) {
throw DbCompat.unexpectedState();
}
DataView view = cloneView();
view.range = subRange(view.range, secondaryKey);
view.dupsKey = view.range.getSingleKey();
view.dupsView = true;
view.keyBinding = primaryKeyBinding;
return view;
}
/**
* Returns a new view with a specified cursor configuration.
*/
DataView configuredView(CursorConfig config) {
DataView view = cloneView();
view.cursorConfig = (config != null) ?
DbCompat.cloneCursorConfig(config) : CursorConfig.DEFAULT;
return view;
}
/**
* Returns the current transaction for the view or null if the environment
* is non-transactional.
*/
CurrentTransaction getCurrentTxn() {
return transactional ? currentTxn : null;
}
/**
* Sets this view's range to a subrange with the given parameters.
*/
private void setRange(Object beginKey, boolean beginInclusive,
Object endKey, boolean endInclusive)
throws DatabaseException, KeyRangeException {
if ((beginKey != null || endKey != null) && !keyRangesAllowed) {
throw new UnsupportedOperationException
("Key ranges allowed only for BTREE databases");
}
KeyRange useRange = useSubRange();
useRange = subRange
(useRange, beginKey, beginInclusive, endKey, endInclusive);
if (dupsView) {
dupsRange = useRange;
} else {
range = useRange;
}
}
/**
* Returns the key thang for a single key range, or null if a single key
* range is not used.
*/
DatabaseEntry getSingleKeyThang() {
return range.getSingleKey();
}
/**
* Returns the environment for the database.
*/
final Environment getEnv() {
return currentTxn.getEnvironment();
}
/**
* Returns whether this is a view on a secondary database rather
* than directly on a primary database.
*/
final boolean isSecondary() {
return (secDb != null);
}
/**
* Returns whether no records are present in the view.
*
* Auto-commit must be performed by the caller.
*/
boolean isEmpty()
throws DatabaseException {
DataCursor cursor = new DataCursor(this, false);
try {
return cursor.getFirst(false) != OperationStatus.SUCCESS;
} finally {
cursor.close();
}
}
/**
* Appends a value and returns the new key. If a key assigner is used
* it assigns the key, otherwise a QUEUE or RECNO database is required.
*
* Auto-commit must be performed by the caller.
*/
OperationStatus append(Object value,
Object[] retPrimaryKey,
Object[] retValue)
throws DatabaseException {
/*
* Flags will be NOOVERWRITE if used with assigner, or APPEND
* otherwise.
* Requires: if value param, value or entity binding
* Requires: if retPrimaryKey, primary key binding (no index).
* Requires: if retValue, value or entity binding
*/
DatabaseEntry keyThang = new DatabaseEntry();
DatabaseEntry valueThang = new DatabaseEntry();
useValue(value, valueThang, null);
OperationStatus status;
if (keyAssigner != null) {
keyAssigner.assignKey(keyThang);
if (!range.check(keyThang)) {
throw new IllegalArgumentException
("assigned key out of range");
}
DataCursor cursor = new DataCursor(this, true);
try {
status = cursor.getCursor().putNoOverwrite(keyThang,
valueThang);
} finally {
cursor.close();
}
} else {
/* Assume QUEUE/RECNO access method. */
if (currentTxn.isCDBCursorOpen(db)) {
throw new IllegalStateException
("cannot open CDB write cursor when read cursor is open");
}
status = DbCompat.append(db, useTransaction(),
keyThang, valueThang);
if (status == OperationStatus.SUCCESS && !range.check(keyThang)) {
db.delete(useTransaction(), keyThang);
throw new IllegalArgumentException
("appended record number out of range");
}
}
if (status == OperationStatus.SUCCESS) {
returnPrimaryKeyAndValue(keyThang, valueThang,
retPrimaryKey, retValue);
}
return status;
}
/**
* Returns the current transaction if the database is transaction, or null
* if the database is not transactional or there is no current transaction.
*/
Transaction useTransaction() {
return transactional ? currentTxn.getTransaction() : null;
}
/**
* Deletes all records in the current range.
*
* Auto-commit must be performed by the caller.
*/
void clear()
throws DatabaseException {
DataCursor cursor = new DataCursor(this, true);
try {
OperationStatus status = OperationStatus.SUCCESS;
while (status == OperationStatus.SUCCESS) {
if (keysRenumbered) {
status = cursor.getFirst(true);
} else {
status = cursor.getNext(true);
}
if (status == OperationStatus.SUCCESS) {
cursor.delete();
}
}
} finally {
cursor.close();
}
}
/**
* Returns a cursor for this view that reads only records having the
* specified index key values.
*/
DataCursor join(DataView[] indexViews, Object[] indexKeys,
JoinConfig joinConfig)
throws DatabaseException {
DataCursor joinCursor = null;
DataCursor[] indexCursors = new DataCursor[indexViews.length];
try {
for (int i = 0; i < indexViews.length; i += 1) {
indexCursors[i] = new DataCursor(indexViews[i], false);
indexCursors[i].getSearchKey(indexKeys[i], null, false);
}
joinCursor = new DataCursor(this, indexCursors, joinConfig, true);
return joinCursor;
} finally {
if (joinCursor == null) {
// An exception is being thrown, so close cursors we opened.
for (int i = 0; i < indexCursors.length; i += 1) {
if (indexCursors[i] != null) {
try { indexCursors[i].close(); }
catch (Exception e) {
/* FindBugs, this is ok. */
}
}
}
}
}
}
/**
* Returns a cursor for this view that reads only records having the
* index key values at the specified cursors.
*/
DataCursor join(DataCursor[] indexCursors, JoinConfig joinConfig)
throws DatabaseException {
return new DataCursor(this, indexCursors, joinConfig, false);
}
/**
* Returns primary key and value if return parameters are non-null.
*/
private void returnPrimaryKeyAndValue(DatabaseEntry keyThang,
DatabaseEntry valueThang,
Object[] retPrimaryKey,
Object[] retValue) {
// Requires: if retPrimaryKey, primary key binding (no index).
// Requires: if retValue, value or entity binding
if (retPrimaryKey != null) {
if (keyBinding == null) {
throw new IllegalArgumentException
("returning key requires primary key binding");
} else if (isSecondary()) {
throw new IllegalArgumentException
("returning key requires unindexed view");
} else {
retPrimaryKey[0] = keyBinding.entryToObject(keyThang);
}
}
if (retValue != null) {
retValue[0] = makeValue(keyThang, valueThang);
}
}
/**
* Populates the key entry and returns whether the key is within range.
*/
boolean useKey(Object key, Object value, DatabaseEntry keyThang,
KeyRange checkRange)
throws DatabaseException {
if (key != null) {
if (keyBinding == null) {
throw new IllegalArgumentException
("non-null key with null key binding");
}
keyBinding.objectToEntry(key, keyThang);
} else {
if (value == null) {
throw new IllegalArgumentException("null key and null value");
}
if (entityBinding == null) {
throw new IllegalStateException
("EntityBinding required to derive key from value");
}
if (!dupsView && isSecondary()) {
DatabaseEntry primaryKeyThang = new DatabaseEntry();
entityBinding.objectToKey(value, primaryKeyThang);
DatabaseEntry valueThang = new DatabaseEntry();
entityBinding.objectToData(value, valueThang);
secKeyCreator.createSecondaryKey(secDb, primaryKeyThang,
valueThang, keyThang);
} else {
entityBinding.objectToKey(value, keyThang);
}
}
if (recNumAccess && DbCompat.getRecordNumber(keyThang) <= 0) {
return false;
}
if (checkRange != null && !checkRange.check(keyThang)) {
return false;
}
return true;
}
/**
* Returns whether data keys can be derived from the value/entity binding
* of this view, which determines whether a value/entity object alone is
* sufficient for operations that require keys.
*/
final boolean canDeriveKeyFromValue() {
return (entityBinding != null);
}
/**
* Populates the value entry and throws an exception if the primary key
* would be changed via an entity binding.
*/
void useValue(Object value, DatabaseEntry valueThang,
DatabaseEntry checkKeyThang) {
if (valueBinding != null) {
/* Allow binding to handle null value. */
valueBinding.objectToEntry(value, valueThang);
} else if (entityBinding != null) {
if (value == null) {
throw new IllegalArgumentException
("null value with entity binding");
}
entityBinding.objectToData(value, valueThang);
if (checkKeyThang != null) {
DatabaseEntry thang = new DatabaseEntry();
entityBinding.objectToKey(value, thang);
if (!KeyRange.equalBytes(thang, checkKeyThang)) {
throw new IllegalArgumentException
("cannot change primary key");
}
}
} else {
if (value != null) {
throw new IllegalArgumentException
("non-null value with null value/entity binding");
}
valueThang.setData(KeyRange.ZERO_LENGTH_BYTE_ARRAY);
valueThang.setOffset(0);
valueThang.setSize(0);
}
}
/**
* Converts a key entry to a key object.
*/
Object makeKey(DatabaseEntry keyThang, DatabaseEntry priKeyThang) {
if (keyBinding == null) {
throw new UnsupportedOperationException();
} else {
DatabaseEntry thang = dupsView ? priKeyThang : keyThang;
if (thang.getSize() == 0) {
return null;
} else {
return keyBinding.entryToObject(thang);
}
}
}
/**
* Converts a key-value entry pair to a value object.
*/
Object makeValue(DatabaseEntry primaryKeyThang, DatabaseEntry valueThang) {
Object value;
if (valueBinding != null) {
value = valueBinding.entryToObject(valueThang);
} else if (entityBinding != null) {
value = entityBinding.entryToObject(primaryKeyThang,
valueThang);
} else {
throw new UnsupportedOperationException
("Requires valueBinding or entityBinding");
}
return value;
}
/**
* Intersects the given key and the current range.
*/
KeyRange subRange(KeyRange useRange, Object singleKey)
throws DatabaseException, KeyRangeException {
return useRange.subRange(makeRangeKey(singleKey));
}
/**
* Intersects the given range and the current range.
*/
KeyRange subRange(KeyRange useRange,
Object beginKey, boolean beginInclusive,
Object endKey, boolean endInclusive)
throws DatabaseException, KeyRangeException {
if (beginKey == endKey && beginInclusive && endInclusive) {
return subRange(useRange, beginKey);
}
if (!ordered) {
throw new UnsupportedOperationException
("Cannot use key ranges on an unsorted database");
}
DatabaseEntry beginThang =
(beginKey != null) ? makeRangeKey(beginKey) : null;
DatabaseEntry endThang =
(endKey != null) ? makeRangeKey(endKey) : null;
return useRange.subRange(beginThang, beginInclusive,
endThang, endInclusive);
}
/**
* Returns the range to use for sub-ranges. Returns range if this is not a
* dupsView, or the dupsRange if this is a dupsView, creating dupsRange if
* necessary.
*/
KeyRange useSubRange()
throws DatabaseException {
if (dupsView) {
synchronized (this) {
if (dupsRange == null) {
DatabaseConfig config =
secDb.getPrimaryDatabase().getConfig();
dupsRange = new KeyRange(config.getBtreeComparator());
}
}
return dupsRange;
} else {
return range;
}
}
/**
* Given a key object, make a key entry that can be used in a range.
*/
private DatabaseEntry makeRangeKey(Object key)
throws DatabaseException {
DatabaseEntry thang = new DatabaseEntry();
if (keyBinding != null) {
useKey(key, null, thang, null);
} else {
useKey(null, key, thang, null);
}
return thang;
}
}