package org.thrudb.thrudoc.tokyocabinet;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.log4j.Logger;
import org.apache.thrift.TException;
import org.thrudb.thrudoc.ThrudocBackend;
import tokyocabinet.BDB;
import tokyocabinet.BDBCUR;
/**
* Implements thrudoc api using TokyoCabinet.
*
* @author jake
*
*/
public class TokyoCabinetDB implements ThrudocBackend {
private Logger logger = Logger.getLogger(getClass());
private String docRoot;
private String bucketName;
private BDB bdb;
/**
* Allocates a new tokyo cabinet bdb.
*
* Works like a key/value store with list functionality
* If the db is not on disk it creates it.
*
* @param docRoot where all the dbs live.
* @param bucketName db name really
*
* @throws TException
*/
public TokyoCabinetDB(String docRoot, String bucketName) throws TException{
this.docRoot = docRoot;
this.bucketName = bucketName;
int bdbFlags = BDB.OWRITER;
//verify db file
String dbFileName = docRoot+File.separatorChar+bucketName+".tcb";
File dbFile = new File(dbFileName);
if(dbFile.isFile() && !dbFile.canWrite())
throw new TException(dbFileName+" is not writable");
if(dbFile.isDirectory())
throw new TException(dbFileName+" should not be a directory");
if(!dbFile.exists())
bdbFlags |= BDB.OCREAT;
bdb = new BDB();
if(!bdb.open(dbFileName,bdbFlags)){
throw new TException(bdb.errmsg());
}
}
/**
* Gets a key from the db.
*
* @param key the key name
* @return the value for this key (binary)
*/
public byte[] get(String key) {
byte[] value = bdb.get(key.getBytes());
return value;
}
/**
* Creates or Replaces a key in the db with a binary value
* @param key the key name
* @param value the binary value
*/
public void put(String key, byte[] value) {
bdb.put(key.getBytes(), value);
}
/**
* Removes a key/value from the db.
*
* @param key the name of key to remove
*/
public void remove(String key) {
bdb.out(key);
}
/**
* Returns a list of keys that have a lexical order greater than the seed.
*
* @param seed the starting key to search from
* @param limit the max results
* @return list of keys greater than the seed
*/
@SuppressWarnings("unchecked")
public List<String> scan(String seed, int limit) {
return bdb.fwmkeys(seed, limit);
}
/**
* Increments a counter by a specified amount.
*
* @param key the counter name
* @param amount the amount to increment by
* @return
*/
public int incr(String key, int amount){
return bdb.addint(key, amount);
}
/**
* Decrements a counter by a specified amount
*
* @param key the counter name
* @param amount the amount to decrement by
* @return
*/
public int decr(String key, int amount){
if(amount > 0)
amount *= -1;
return bdb.addint(key, amount);
}
@SuppressWarnings("unchecked")
public void push_back(String key, byte[] value) throws TException {
//first get the next key.
List<byte[]> nextKey = bdb.fwmkeys(key.getBytes(),2);
BDBCUR cursor = new BDBCUR(bdb);
//look for edge cases
//no forward keys
if(nextKey.isEmpty() ){
this.put(key, value);
return;
}
//the input is all that was found. jump to last record.
if( Arrays.equals(nextKey.get(0),key.getBytes()) && nextKey.size() == 1){
if(!cursor.last())
throw new RuntimeException("Can't jump to last record");
}else{
//jump to key ahead of our input key
if(nextKey.size() == 1){
if(!cursor.jump(nextKey.get(0)))
throw new RuntimeException("Key should exist but failed to jump to it's location");
} else {
if(!cursor.jump(nextKey.get(1)))
throw new RuntimeException("Key should exist but failed to jump to it's location");
}
}
//back one should be the input key, if it's not then the key
//does not exist yet, so we add it
if(!Arrays.equals(cursor.key(),key.getBytes())){
if(!cursor.prev()){
this.put(key, value);
return;
}
}
// the key under this cursor should match, if not we add a new key
//based on input
if(!Arrays.equals(cursor.key(),key.getBytes())){
this.put(key, value);
return;
}
//we are at the right spot, now append the value
if(!cursor.put(value, BDBCUR.CPAFTER))
throw new TException(bdb.errmsg());
}
@SuppressWarnings("unchecked")
public byte[] pop_back(String key) throws TException{
//first get the next key.
List<byte[]> nextKey = bdb.fwmkeys(key.getBytes(),2);
BDBCUR cursor = new BDBCUR(bdb);
if(nextKey.isEmpty()){
return new byte[]{};
}
//first entry should match, if not the input key is missing so fail
if(!Arrays.equals(nextKey.get(0),key.getBytes())){
return new byte[]{};
}
//the input key is at the end of the index so jump to end
if(nextKey.size() == 1) {
if(!cursor.last())
throw new RuntimeException("Unable to jump to last key");
} else {
if(!cursor.jump(nextKey.get(1)))
throw new RuntimeException("Key should exist but failed to jump to it's location");
}
//move back one, to tail of the input key
if(!cursor.prev())
throw new RuntimeException("Key should exist but failed to jump to it's location");
if(!Arrays.equals(cursor.key(),key.getBytes()))
throw new TException("key mismatch "+key+" vs "+cursor.key2());
byte[] value = cursor.val();
//delete
if(!cursor.out())
throw new TException(bdb.errmsg());
return value;
}
public void push_front(String key, byte[] value) throws TException {
BDBCUR cursor = new BDBCUR(bdb);
if(!cursor.jump(key)){
this.put(key, value);
return;
}
//not matching key so add new
if(!Arrays.equals(cursor.key(),key.getBytes())){
this.put(key, value);
return;
}
if(!cursor.put(value, BDBCUR.CPBEFORE))
throw new TException(bdb.errmsg());
}
public byte[] pop_front(String key) throws TException{
BDBCUR cursor = new BDBCUR(bdb);
if(!cursor.jump(key))
return new byte[]{};
if(!Arrays.equals(cursor.key(),key.getBytes()))
return new byte[]{};
//save value
byte[] value = cursor.val();
//delete
if(!cursor.out())
throw new TException(bdb.errmsg());
return value;
}
private BDBCUR goto_position(String key, int position) {
if(position < 0)
return null;
BDBCUR cursor = new BDBCUR(bdb);
if(!cursor.jump(key))
return null;
int currentPos = 0;
int checkMod = 10; //verify key every few records
while(currentPos < position){
cursor.next();
if(currentPos % checkMod == 0){
if(!Arrays.equals(cursor.key(), key.getBytes()))
return null;
}
currentPos++;
}
//at position
if(!Arrays.equals(cursor.key(), key.getBytes()))
return null;
return cursor;
}
public byte[] remove_at(String key, int position){
BDBCUR cursor = this.goto_position(key, position);
//if position not found
if(cursor == null)
return null;
byte[] value = cursor.val();
if(!cursor.out())
throw new RuntimeException("Unable to remove record");
return value;
}
public void insert_at(String key, byte[] value, int position) {
BDBCUR cursor = this.goto_position(key, position);
if(cursor == null)
throw new RuntimeException("Unable to insert at position: "+position);
if(!cursor.put(value, BDBCUR.CPBEFORE))
throw new RuntimeException("Unable to replace record");
}
public void replace_at(String key, byte[] value, int position) {
BDBCUR cursor = this.goto_position(key, position);
if(cursor == null)
return;
if(!cursor.put(value, BDBCUR.CPCURRENT))
throw new RuntimeException("Unable to replace record");
}
public byte[] retrieve_at(String key, int position){
BDBCUR cursor = this.goto_position(key, position);
//if position not found
if(cursor == null)
return null;
byte[] value = cursor.val();
return value;
}
public List<byte[]> range(String key, int start, int end){
if(start > end || end < 0 || start < 0)
throw new RuntimeException("Invalid start and/or end context");
BDBCUR cursor = this.goto_position(key, start);
if(cursor == null)
return null;
int currentPos = 0;
int distance = end - start;
List<byte[]> response = new ArrayList<byte[]>();
while(currentPos <= distance){
response.add(cursor.val());
if(!cursor.next())
break;
currentPos++;
}
return response;
}
public int length(String key) {
BDBCUR cursor = new BDBCUR(bdb);
if(!cursor.jump(key))
return 0;
if(!Arrays.equals(cursor.key(),key.getBytes()))
return 0;
int length = 0;
while(Arrays.equals(cursor.key(),key.getBytes())){
length++;
if(!cursor.next())
return length;
}
return length;
}
public boolean erase(){
return bdb.vanish();
}
}