package net.jxta.impl.cm.srdi.bdb;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import net.jxta.impl.cm.CacheUtils;
import net.jxta.impl.cm.SrdiAPI;
import net.jxta.impl.cm.Srdi.Entry;
import net.jxta.impl.cm.bdb.BerkeleyDbUtil;
import net.jxta.impl.util.TimeUtils;
import net.jxta.logging.Logging;
import net.jxta.peer.PeerID;
import net.jxta.peergroup.PeerGroup;
import net.jxta.peergroup.PeerGroupID;
import com.sleepycat.bind.tuple.LongBinding;
import com.sleepycat.bind.tuple.TupleBinding;
import com.sleepycat.je.Cursor;
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.EnvironmentConfig;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.SecondaryConfig;
import com.sleepycat.je.SecondaryDatabase;
import com.sleepycat.util.IOExceptionWrapper;
public class BerkeleyDbSrdiIndexBackend implements SrdiAPI {
private static final String DB_NAME = "mainIndex";
private static final String PEER_SEARCH_DB_NAME = "peerSearch";
private static final String EXPIRY_SEARCH_DB_NAME = "expirySearch";
public static final long DEFAULT_GC_INTERVAL = TimeUtils.ANHOUR;
private static final Logger LOG = Logger.getLogger(BerkeleyDbSrdiIndexBackend.class.getName());
private PeerGroupID groupId;
private String indexName;
private Environment dbEnvironment;
private Database db;
private SecondaryDatabase peerSearchDb;
private SecondaryDatabase expirySearchDb;
private boolean open;
public BerkeleyDbSrdiIndexBackend(PeerGroup group, String indexName) throws IOException {
this(group, indexName, DEFAULT_GC_INTERVAL);
}
public BerkeleyDbSrdiIndexBackend(PeerGroup group, String indexName, long gcInterval) throws IOException {
this(new File(group.getStoreHome()), group.getPeerGroupID(), indexName, gcInterval);
}
public BerkeleyDbSrdiIndexBackend(File storeHome, PeerGroupID groupId, String indexName) throws IOException {
this(storeHome, groupId, indexName, DEFAULT_GC_INTERVAL);
}
public BerkeleyDbSrdiIndexBackend(File storeHome, PeerGroupID groupId, String indexName, long gcInterval) throws IOException {
this.groupId = groupId;
this.indexName = indexName;
File srdiHome = createStoreDir(storeHome);
try {
EnvironmentConfig envConfig = new EnvironmentConfig();
envConfig.setAllowCreate(true);
envConfig.setSharedCache(true);
envConfig.setTransactional(false);
dbEnvironment = new Environment(srdiHome, envConfig);
DatabaseConfig dbConfig = new DatabaseConfig();
dbConfig.setAllowCreate(true);
dbConfig.setDeferredWrite(true);
db = dbEnvironment.openDatabase(null, DB_NAME, dbConfig);
SecondaryConfig peerSearchDbConfig = new SecondaryConfig();
peerSearchDbConfig.setAllowCreate(true);
peerSearchDbConfig.setAllowPopulate(true);
peerSearchDbConfig.setSortedDuplicates(true);
peerSearchDbConfig.setKeyCreator(new PeerSearchKeyCreator());
peerSearchDb = dbEnvironment.openSecondaryDatabase(null, PEER_SEARCH_DB_NAME, db, peerSearchDbConfig);
SecondaryConfig expirySearchDbConfig = new SecondaryConfig();
expirySearchDbConfig.setAllowCreate(true);
expirySearchDbConfig.setAllowPopulate(true);
expirySearchDbConfig.setSortedDuplicates(true);
expirySearchDbConfig.setKeyCreator(new ExpirySearchKeyCreator());
expirySearchDb = dbEnvironment.openSecondaryDatabase(null, EXPIRY_SEARCH_DB_NAME, db, expirySearchDbConfig);
garbageCollect();
open = true;
} catch(Exception e) {
stop();
throw new IOExceptionWrapper(e);
}
}
private File createStoreDir(File storeHome) throws IOException {
File srdiRootFile = new File(storeHome, "bdb_srdi_index");
if(!srdiRootFile.exists()) {
if(!srdiRootFile.mkdirs()) {
throw new IOException("Failed to create directories for BDB SRDI Index at " + srdiRootFile.getAbsolutePath());
}
}
if(!srdiRootFile.isDirectory()) {
throw new IOException("Provided store root URI does not point to a directory: " + srdiRootFile.getAbsolutePath());
}
return srdiRootFile;
}
public static void clearSrdi(PeerGroup group) {
try {
BerkeleyDbSrdiIndexBackend backend = new BerkeleyDbSrdiIndexBackend(group, "CLEARALL");
backend.clearAllIndices(group);
} catch (IOException e) {
Logging.logCheckedWarning(LOG, "Error occurred while clearing Srdi indices for group [" + group + "]\n", e);
}
}
private void clearAllIndices(PeerGroup group) throws IOException {
SrdiIndexKey searchKey = new SrdiIndexKey(group.getPeerGroupID());
SrdiIndexKeyTupleBinding binding = new SrdiIndexKeyTupleBinding();
processAllMatching(searchKey, db, binding, new DeleteMatchHandler<SrdiIndexKey>());
}
public void add(String primaryKey, String attribute, String value, PeerID pid, long expiration) throws IOException {
DatabaseEntry key = new SrdiIndexKey(groupId, indexName, primaryKey, attribute, value, pid).toDatabaseEntry();
DatabaseEntry data = new DatabaseEntry();
LongBinding.longToEntry(TimeUtils.toAbsoluteTimeMillis(expiration), data);
try {
db.put(null, key, data);
} catch (DatabaseException e) {
throw new IOExceptionWrapper(e);
}
}
public void clear() throws IOException {
SrdiIndexKey searchKey = new SrdiIndexKey(groupId, indexName);
SrdiIndexKeyTupleBinding binding = new SrdiIndexKeyTupleBinding();
processAllMatching(searchKey, db, binding, new DeleteMatchHandler<SrdiIndexKey>());
}
private <T> void processAllMatching(T prefix, Database database, TupleBinding<T> binding, MatchHandler<T> matchHandler) throws IOExceptionWrapper {
Cursor c = null;
try {
c = database.openCursor(null, CursorConfig.READ_UNCOMMITTED);
DatabaseEntry searchKey = new DatabaseEntry();
binding.objectToEntry(prefix, searchKey);
DatabaseEntry key = new DatabaseEntry();
binding.objectToEntry(prefix, key);
DatabaseEntry data = new DatabaseEntry();
OperationStatus result = c.getSearchKeyRange(key, data, null);
while(result == OperationStatus.SUCCESS) {
if(!BerkeleyDbUtil.isPrefixOf(searchKey, key)) {
break;
}
if(!matchHandler.handleMatch(c, binding.entryToObject(key), data)) {
break;
}
result = c.getNext(key, data, null);
}
} catch (DatabaseException e) {
throw new IOExceptionWrapper(e);
} finally {
closeCursor(c, "Failed to close cursor while clearing");
}
}
private interface MatchHandler<T> {
public boolean handleMatch(Cursor cursor, T matchingKey, DatabaseEntry matchingData) throws DatabaseException;
}
private class DeleteMatchHandler<T> implements MatchHandler<T> {
public boolean handleMatch(Cursor cursor, T matchingKey, DatabaseEntry matchingData) throws DatabaseException {
cursor.delete();
return true;
}
}
private abstract class QueryMatchHandler<T, U, V extends Collection<U>> implements MatchHandler<T> {
protected V matches;
public QueryMatchHandler(V matches) {
this.matches = matches;
}
public abstract boolean handleMatch(Cursor cursor, T matchingKey, DatabaseEntry matchingData) throws DatabaseException;
}
private class ValueRegexMatchHandler extends QueryMatchHandler<SrdiIndexKey,PeerID, Set<PeerID>> {
private String regex;
private int threshold;
public ValueRegexMatchHandler(Set<PeerID> matches, String regex, int threshold) {
super(matches);
this.regex = regex;
this.threshold = threshold;
}
@Override
public boolean handleMatch(Cursor cursor, SrdiIndexKey matchingKey,
DatabaseEntry matchingData) throws DatabaseException {
if(matchingKey.getValue().matches(regex)) {
matches.add(matchingKey.getPeerId());
}
return matches.size() < threshold;
}
}
private class AllMatchHandler extends QueryMatchHandler<SrdiIndexKey, PeerID, Set<PeerID>> {
private int threshold;
public AllMatchHandler(Set<PeerID> results, int threshold) {
super(results);
this.threshold = threshold;
}
public boolean handleMatch(Cursor cursor, SrdiIndexKey matchingKey, DatabaseEntry matchingData) throws DatabaseException {
matches.add(matchingKey.getPeerId());
return matches.size() < threshold;
}
}
public void garbageCollect() throws IOException {
Cursor c = null;
try {
c = expirySearchDb.openSecondaryCursor(null, CursorConfig.READ_UNCOMMITTED);
DatabaseEntry key = new DatabaseEntry();
DatabaseEntry data = new DatabaseEntry();
OperationStatus result = c.getFirst(key, data, null);
while(result == OperationStatus.SUCCESS) {
long expiryTime = LongBinding.entryToLong(key);
if(expiryTime > TimeUtils.timeNow()) {
break;
}
c.delete();
result = c.getNext(key, data, null);
}
} catch (DatabaseException e) {
throw new IOExceptionWrapper(e);
} finally {
closeCursor(c, "Failed to close cursor when garbage collecting");
}
}
public List<Entry> getRecord(String primaryKey, String attribute, String value) throws IOException {
LinkedList<Entry> results = new LinkedList<Entry>();
Cursor cursor = null;
try {
cursor = db.openCursor(null, CursorConfig.READ_UNCOMMITTED);
DatabaseEntry searchKey = new SrdiIndexKey(groupId, indexName, primaryKey, attribute, value, null).toDatabaseEntry();
DatabaseEntry key = new SrdiIndexKey(groupId, indexName, primaryKey, attribute, value, null).toDatabaseEntry();
DatabaseEntry data = new DatabaseEntry();
OperationStatus searchResult = cursor.getSearchKeyRange(key, data, null);
while(searchResult == OperationStatus.SUCCESS) {
if(!BerkeleyDbUtil.isPrefixOf(searchKey, key)) {
break;
}
SrdiIndexKey matchingKey = SrdiIndexKey.fromDatabaseEntry(key);
long expiry = LongBinding.entryToLong(data);
results.add(new Entry(matchingKey.getPeerId(), expiry));
searchResult = cursor.getNext(key, data, null);
}
} catch (DatabaseException e) {
throw new IOExceptionWrapper(e);
} finally {
closeCursor(cursor, "Failed to close cursor when retrieving records");
}
return results;
}
public List<PeerID> query(String primaryKey, String attribute, String value, int threshold) throws IOException {
Set<PeerID> results = new HashSet<PeerID>();
SrdiIndexKey searchKey = new SrdiIndexKey(groupId, indexName, primaryKey, attribute, null, null);
QueryMatchHandler<SrdiIndexKey, PeerID, Set<PeerID>> handler;
if(value == null) {
handler = new AllMatchHandler(results, threshold);
} else if(!CacheUtils.hasWildcards(value)) {
searchKey.setValue(value);
handler = new AllMatchHandler(results, threshold);
} else {
String regex = CacheUtils.convertValueQueryToRegex(value);
handler = new ValueRegexMatchHandler(results, regex, threshold);
}
processAllMatching(searchKey, db, new SrdiIndexKeyTupleBinding(), handler);
return new ArrayList<PeerID>(results);
}
public void remove(PeerID pid) throws IOException {
PeerSearchKey searchCriteria = new PeerSearchKey(groupId, indexName, pid);
PeerSearchKeyTupleBinding binding = new PeerSearchKeyTupleBinding();
processAllMatching(searchCriteria, peerSearchDb, binding, new DeleteMatchHandler<PeerSearchKey>());
}
public void stop() {
if(!open) {
return;
}
closeDatabase(expirySearchDb, EXPIRY_SEARCH_DB_NAME);
closeDatabase(peerSearchDb, PEER_SEARCH_DB_NAME);
closeDatabase(db, DB_NAME);
if(dbEnvironment != null) {
try {
dbEnvironment.close();
} catch(DatabaseException e) {
Logging.logCheckedSevere(LOG, "Failed to close SRDI index environment when stopping SRDI index " + indexName, e);
}
}
open = false;
}
private void closeCursor(Cursor c, String failMessage) {
if(c != null) {
try {
c.close();
} catch(DatabaseException e) {
Logging.logCheckedWarning(LOG, failMessage, e);
}
}
}
private void closeDatabase(Database db, String name) {
if(db != null) {
try {
db.close();
} catch(DatabaseException e) {
Logging.logCheckedSevere(LOG, "Failed to close BDB " + name
+ " when stopping SRDI index " + indexName, e);
}
}
}
}