/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002, 2010 Oracle and/or its affiliates. All rights reserved.
*
*/
package com.sleepycat.persist;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.sleepycat.bind.EntityBinding;
import com.sleepycat.bind.EntryBinding;
import com.sleepycat.db.Cursor;
import com.sleepycat.db.CursorConfig;
import com.sleepycat.db.Database;
import com.sleepycat.db.DatabaseEntry;
import com.sleepycat.db.DatabaseException;
import com.sleepycat.db.JoinCursor;
import com.sleepycat.db.LockMode;
import com.sleepycat.db.OperationStatus;
import com.sleepycat.db.Transaction;
/**
* Performs an equality join on two or more secondary keys.
*
*
{@code EntityJoin} objects are thread-safe. Multiple threads may safely
* call the methods of a shared {@code EntityJoin} object.
*
* An equality join is a match on all entities in a given primary index that
* have two or more specific secondary key values. Note that key ranges may
* not be matched by an equality join, only exact keys are matched.
*
* For example:
*
* // Index declarations -- see {@link package summary example}.
* //
* {@literal PrimaryIndex personBySsn;}
* {@literal SecondaryIndex personByParentSsn;}
* {@literal SecondaryIndex personByEmployerIds;}
* Employer employer = ...;
*
* // Match on all Person objects having parentSsn "111-11-1111" and also
* // containing an employerId of employer.id. In other words, match on all
* // of Bob's children that work for a given employer.
* //
* {@literal EntityJoin join = new EntityJoin(personBySsn);}
* join.addCondition(personByParentSsn, "111-11-1111");
* join.addCondition(personByEmployerIds, employer.id);
*
* // Perform the join operation by traversing the results with a cursor.
* //
* {@literal ForwardCursor results = join.entities();}
* try {
* for (Person person : results) {
* System.out.println(person.ssn + ' ' + person.name);
* }
* } finally {
* results.close();
* }
*
* @author Mark Hayes
*/
public class EntityJoin {
private PrimaryIndex primary;
private List conditions;
/**
* Creates a join object for a given primary index.
*
* @param index the primary index on which the join will operate.
*/
public EntityJoin(PrimaryIndex index) {
primary = index;
conditions = new ArrayList();
}
/**
* Adds a secondary key condition to the equality join. Only entities
* having the given key value in the given secondary index will be returned
* by the join operation.
*
* @param index the secondary index containing the given key value.
*
* @param key the key value to match during the join.
*/
public void addCondition(SecondaryIndex index, SK key) {
/* Make key entry. */
DatabaseEntry keyEntry = new DatabaseEntry();
index.getKeyBinding().objectToEntry(key, keyEntry);
/* Use keys database if available. */
Database db = index.getKeysDatabase();
if (db == null) {
db = index.getDatabase();
}
/* Add condition. */
conditions.add(new Condition(db, keyEntry));
}
/**
* Opens a cursor that returns the entities qualifying for the join. The
* join operation is performed as the returned cursor is accessed.
*
* The operations performed with the cursor will not be transaction
* protected, and {@link CursorConfig#DEFAULT} is used implicitly.
*
* @return the cursor.
*
*
* @throws IllegalStateException if less than two conditions were added.
*
* @throws DatabaseException the base class for all BDB exceptions.
*/
public ForwardCursor entities()
throws DatabaseException {
return entities(null, null);
}
/**
* Opens a cursor that returns the entities qualifying for the join. The
* join operation is performed as the returned cursor is accessed.
*
* @param txn the transaction used to protect all operations performed with
* the cursor, or null if the operations should not be transaction
* protected. If the store is non-transactional, null must be specified.
* For a transactional store the transaction is optional for read-only
* access and required for read-write access.
*
* @param config the cursor configuration that determines the default lock
* mode used for all cursor operations, or null to implicitly use {@link
* CursorConfig#DEFAULT}.
*
* @return the cursor.
*
*
* @throws IllegalStateException if less than two conditions were added.
*
* @throws DatabaseException the base class for all BDB exceptions.
*/
public ForwardCursor entities(Transaction txn, CursorConfig config)
throws DatabaseException {
return new JoinForwardCursor(txn, config, false);
}
/**
* Opens a cursor that returns the primary keys of entities qualifying for
* the join. The join operation is performed as the returned cursor is
* accessed.
*
* The operations performed with the cursor will not be transaction
* protected, and {@link CursorConfig#DEFAULT} is used implicitly.
*
* @return the cursor.
*
*
* @throws IllegalStateException if less than two conditions were added.
*
* @throws DatabaseException the base class for all BDB exceptions.
*/
public ForwardCursor keys()
throws DatabaseException {
return keys(null, null);
}
/**
* Opens a cursor that returns the primary keys of entities qualifying for
* the join. The join operation is performed as the returned cursor is
* accessed.
*
* @param txn the transaction used to protect all operations performed with
* the cursor, or null if the operations should not be transaction
* protected. If the store is non-transactional, null must be specified.
* For a transactional store the transaction is optional for read-only
* access and required for read-write access.
*
* @param config the cursor configuration that determines the default lock
* mode used for all cursor operations, or null to implicitly use {@link
* CursorConfig#DEFAULT}.
*
* @return the cursor.
*
*
* @throws IllegalStateException if less than two conditions were added.
*
* @throws DatabaseException the base class for all BDB exceptions.
*/
public ForwardCursor keys(Transaction txn, CursorConfig config)
throws DatabaseException {
return new JoinForwardCursor(txn, config, true);
}
private static class Condition {
private Database db;
private DatabaseEntry key;
Condition(Database db, DatabaseEntry key) {
this.db = db;
this.key = key;
}
Cursor openCursor(Transaction txn, CursorConfig config)
throws DatabaseException {
OperationStatus status;
Cursor cursor = db.openCursor(txn, config);
try {
DatabaseEntry data = BasicIndex.NO_RETURN_ENTRY;
status = cursor.getSearchKey(key, data, null);
} catch (DatabaseException e) {
try {
cursor.close();
} catch (DatabaseException ignored) {}
throw e;
}
if (status == OperationStatus.SUCCESS) {
return cursor;
} else {
cursor.close();
return null;
}
}
}
private class JoinForwardCursor implements ForwardCursor {
private Cursor[] cursors;
private JoinCursor joinCursor;
private boolean doKeys;
JoinForwardCursor(Transaction txn, CursorConfig config, boolean doKeys)
throws DatabaseException {
this.doKeys = doKeys;
try {
cursors = new Cursor[conditions.size()];
for (int i = 0; i < cursors.length; i += 1) {
Condition cond = conditions.get(i);
Cursor cursor = cond.openCursor(txn, config);
if (cursor == null) {
/* Leave joinCursor null. */
doClose(null);
return;
}
cursors[i] = cursor;
}
joinCursor = primary.getDatabase().join(cursors, null);
} catch (DatabaseException e) {
/* doClose will throw e. */
doClose(e);
}
}
public V next()
throws DatabaseException {
return next(null);
}
public V next(LockMode lockMode)
throws DatabaseException {
if (joinCursor == null) {
return null;
}
if (doKeys) {
DatabaseEntry key = new DatabaseEntry();
OperationStatus status = joinCursor.getNext(key, lockMode);
if (status == OperationStatus.SUCCESS) {
EntryBinding binding = primary.getKeyBinding();
return (V) binding.entryToObject(key);
}
} else {
DatabaseEntry key = new DatabaseEntry();
DatabaseEntry data = new DatabaseEntry();
OperationStatus status =
joinCursor.getNext(key, data, lockMode);
if (status == OperationStatus.SUCCESS) {
EntityBinding binding = primary.getEntityBinding();
return (V) binding.entryToObject(key, data);
}
}
return null;
}
public Iterator iterator() {
return iterator(null);
}
public Iterator iterator(LockMode lockMode) {
return new BasicIterator(this, lockMode);
}
public void close()
throws DatabaseException {
doClose(null);
}
private void doClose(DatabaseException firstException)
throws DatabaseException {
if (joinCursor != null) {
try {
joinCursor.close();
joinCursor = null;
} catch (DatabaseException e) {
if (firstException == null) {
firstException = e;
}
}
}
for (int i = 0; i < cursors.length; i += 1) {
Cursor cursor = cursors[i];
if (cursor != null) {
try {
cursor.close();
cursors[i] = null;
} catch (DatabaseException e) {
if (firstException == null) {
firstException = e;
}
}
}
}
if (firstException != null) {
throw firstException;
}
}
}
}