Added support for gzipped txlog files and org.apache.jdbm as backing

store
This commit is contained in:
Eyck Jentzsch 2017-01-23 21:23:43 +01:00
parent c8a213e985
commit 1aaf5c837e
48 changed files with 14835 additions and 22 deletions

View File

@ -0,0 +1 @@
/.scviewer.*

View File

@ -10,7 +10,12 @@
*******************************************************************************/
package com.minres.scviewer.database.text;
import java.nio.charset.CharsetDecoder;
import java.util.Collection;
import java.util.zip.GZIPInputStream
import org.apache.jdbm.DB
import org.apache.jdbm.DBMaker
import groovy.io.FileType
import com.minres.scviewer.database.AssociationType
import com.minres.scviewer.database.DataType
@ -27,6 +32,8 @@ public class TextDbLoader implements IWaveformDbLoader{
IWaveformDb db;
DB backingDb;
def streams = []
def relationTypes=[:]
@ -56,18 +63,54 @@ public class TextDbLoader implements IWaveformDbLoader{
boolean load(IWaveformDb db, File file) throws Exception {
this.db=db
this.streams=[]
FileInputStream fis = new FileInputStream(file)
byte[] buffer = new byte[x.size()]
def readCnt = fis.read(buffer, 0, x.size())
fis.close()
if(readCnt==x.size())
for(int i=0; i<x.size(); i++)
if(buffer[i]!=x[i]) return false
parseInput(file)
calculateConcurrencyIndicees()
def gzipped = isGzipped(file)
if(isTxfile(gzipped?new GZIPInputStream(new FileInputStream(file)):new FileInputStream(file))){
if(true) {
def parentDir=file.absoluteFile.parent
def filename=file.name
new File(parentDir).eachFileRecurse (FileType.FILES) { f -> if(f.name=~/^\.${filename}/) f.delete() }
this.backingDb = DBMaker.openFile(parentDir+File.separator+"."+filename+"_bdb")
.deleteFilesAfterClose()
.useRandomAccessFile()
.setMRUCacheSize(4096)
//.disableTransactions()
.disableLocking()
.make();
} else {
this.backingDb = DBMaker.openMemory().disableLocking().make()
}
parseInput(gzipped?new GZIPInputStream(new FileInputStream(file)):new FileInputStream(file))
calculateConcurrencyIndicees()
return true
}
return false;
}
private static boolean isTxfile(InputStream istream) {
byte[] buffer = new byte[x.size()]
def readCnt = istream.read(buffer, 0, x.size())
istream.close()
if(readCnt==x.size()){
for(int i=0; i<x.size(); i++)
if(buffer[i]!=x[i]) return false
}
return true
}
private static boolean isGzipped(File f) {
InputStream is = null;
try {
is = new FileInputStream(f);
byte [] signature = new byte[2];
int nread = is.read( signature ); //read the gzip signature
return nread == 2 && signature[ 0 ] == (byte) 0x1f && signature[ 1 ] == (byte) 0x8b;
} catch (IOException e) {
return false;
} finally {
is.close()
}
}
private stringToScale(String scale){
switch(scale.trim()){
case "fs":return 1L
@ -78,7 +121,7 @@ public class TextDbLoader implements IWaveformDbLoader{
case "s": return 1000000000000000L
}
}
private def parseInput(File input){
private def parseInput(InputStream inputStream){
def streamsById = [:]
def generatorsById = [:]
def transactionsById = [:]
@ -86,7 +129,9 @@ public class TextDbLoader implements IWaveformDbLoader{
Tx transaction
boolean endTransaction=false
def matcher
input.eachLine { line ->
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
long lineCnt=0;
reader.eachLine { line ->
def tokens = line.split(/\s+/)
switch(tokens[0]){
case "scv_tr_stream":
@ -95,7 +140,7 @@ public class TextDbLoader implements IWaveformDbLoader{
case "end_attribute":
if ((matcher = line =~ /^scv_tr_stream\s+\(ID (\d+),\s+name\s+"([^"]+)",\s+kind\s+"([^"]+)"\)$/)) {
def id = Integer.parseInt(matcher[0][1])
def stream = new TxStream(db, id, matcher[0][2], matcher[0][3])
def stream = new TxStream(db, id, matcher[0][2], matcher[0][3], backingDb)
streams<<stream
streamsById[id]=stream
} else if ((matcher = line =~ /^scv_tr_generator\s+\(ID\s+(\d+),\s+name\s+"([^"]+)",\s+scv_tr_stream\s+(\d+),$/)) {
@ -156,6 +201,10 @@ public class TextDbLoader implements IWaveformDbLoader{
println "Don't know what to do with: '$line'"
}
lineCnt++
if((lineCnt%1000) == 0) {
backingDb.commit()
}
}
}
@ -169,3 +218,4 @@ public class TextDbLoader implements IWaveformDbLoader{
}
}

View File

@ -16,8 +16,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import com.google.common.collect.TreeMultimap
import org.apache.jdbm.DB
import com.minres.scviewer.database.ITxEvent;
import com.minres.scviewer.database.IWaveform;
import com.minres.scviewer.database.IWaveformDb
@ -44,14 +43,15 @@ class TxStream extends HierNode implements ITxStream {
private TreeMap<Long, List<ITxEvent>> events
TxStream(IWaveformDb db, int id, String name, String kind){
TxStream(IWaveformDb db, int id, String name, String kind, DB backingStore){
super(name)
this.id=id
this.database=db
this.fullName=name
this.kind=kind
this.maxConcurrency=0
events = new TreeMap<Long, List<ITxEvent>>()
//events = new TreeMap<Long, List<ITxEvent>>()
events=backingStore.createTreeMap("stream-"+name)
}
List<ITxGenerator> getGenerators(){

View File

@ -0,0 +1,706 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.*;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* B+Tree persistent indexing data structure. B+Trees are optimized for
* block-based, random I/O storage because they store multiple keys on
* one tree node (called <code>BTreeNode</code>). In addition, the leaf nodes
* directly contain (inline) small values associated with the keys, allowing a
* single (or sequential) disk read of all the values on the node.
* <p/>
* B+Trees are n-airy, yeilding log(N) search cost. They are self-balancing,
* preventing search performance degradation when the size of the tree grows.
* <p/>
* BTree stores its keys sorted. By default JDBM expects key to implement
* <code>Comparable</code> interface but user may supply its own <code>Comparator</code>
* at BTree creation time. Comparator is serialized and stored as part of BTree.
* <p/>
* The B+Tree allows traversing the keys in forward and reverse order using a
* TupleBrowser obtained from the browse() methods. But it is better to use
* <code>BTreeMap</code> wrapper which implements <code>SortedMap</code> interface
* <p/>
* This implementation does not directly support duplicate keys. It is
* possible to handle duplicates by grouping values using an ArrayList as value.
* This scenario is supported by JDBM serialization so there is no big performance penalty.
* <p/>
* There is no limit on key size or value size, but it is recommended to keep
* keys as small as possible to reduce disk I/O. If serialized value exceeds 32 bytes,
* it is stored in separate record and tree contains only recid reference to it.
* BTree uses delta compression for its keys.
*
*
* @author Alex Boisvert
* @author Jan Kotek
*/
class BTree<K, V> {
private static final boolean DEBUG = false;
/**
* Default node size (number of entries per node)
*/
public static final int DEFAULT_SIZE = 32; //TODO test optimal size, it has serious impact on sequencial write and read
/**
* Record manager used to persist changes in BTreeNodes
*/
protected transient DBAbstract _db;
/**
* This BTree's record ID in the DB.
*/
private transient long _recid;
/**
* Comparator used to index entries (optional)
*/
protected Comparator<K> _comparator;
/**
* Serializer used to serialize index keys (optional)
*/
protected Serializer<K> keySerializer;
/**
* Serializer used to serialize index values (optional)
*/
protected Serializer<V> valueSerializer;
/**
* indicates if values should be loaded during deserialization, set to false during defragmentation
*/
boolean loadValues = true;
/** if false map contains only keys, used for set*/
boolean hasValues = true;
/**
* The number of structural modifications to the tree for fail fast iterators. This value is just for runtime, it is not persisted
*/
transient int modCount = 0;
/**
* cached instance of an insert result, so we do not have to allocate new object on each insert
*/
protected BTreeNode.InsertResult<K, V> insertResultReuse; //TODO investigate performance impact of removing this
public Serializer<K> getKeySerializer() {
return keySerializer;
}
public Serializer<V> getValueSerializer() {
return valueSerializer;
}
/**
* Height of the B+Tree. This is the number of BTreeNodes you have to traverse
* to get to a leaf BTreeNode, starting from the root.
*/
private int _height;
/**
* Recid of the root BTreeNode
*/
private transient long _root;
/**
* Total number of entries in the BTree
*/
protected volatile long _entries;
/**
* Serializer used for BTreeNodes of this tree
*/
private transient BTreeNode<K, V> _nodeSerializer = new BTreeNode();
{
_nodeSerializer._btree = this;
}
/**
* Listeners which are notified about changes in records
*/
protected RecordListener[] recordListeners = new RecordListener[0];
final protected ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* No-argument constructor used by serialization.
*/
public BTree() {
// empty
}
/**
* Create a new persistent BTree
*/
@SuppressWarnings("unchecked")
public static <K extends Comparable, V> BTree<K, V> createInstance(DBAbstract db)
throws IOException {
return createInstance(db, null, null, null,true);
}
/**
* Create a new persistent BTree
*/
public static <K, V> BTree<K, V> createInstance(DBAbstract db,
Comparator<K> comparator,
Serializer<K> keySerializer,
Serializer<V> valueSerializer,
boolean hasValues)
throws IOException {
BTree<K, V> btree;
if (db == null) {
throw new IllegalArgumentException("Argument 'db' is null");
}
btree = new BTree<K, V>();
btree._db = db;
btree._comparator = comparator;
btree.keySerializer = keySerializer;
btree.valueSerializer = valueSerializer;
btree.hasValues = hasValues;
btree._recid = db.insert(btree, btree.getRecordManager().defaultSerializer(),false);
return btree;
}
/**
* Load a persistent BTree.
*
* @param db DB used to store the persistent btree
* @param recid Record id of the BTree
*/
@SuppressWarnings("unchecked")
public static <K, V> BTree<K, V> load(DBAbstract db, long recid)
throws IOException {
BTree<K, V> btree = (BTree<K, V>) db.fetch(recid);
btree._recid = recid;
btree._db = db;
btree._nodeSerializer = new BTreeNode<K, V>();
btree._nodeSerializer._btree = btree;
return btree;
}
/**
* Get the {@link ReadWriteLock} associated with this BTree.
* This should be used with browsing operations to ensure
* consistency.
*
* @return
*/
public ReadWriteLock getLock() {
return lock;
}
/**
* Insert an entry in the BTree.
* <p/>
* The BTree cannot store duplicate entries. An existing entry can be
* replaced using the <code>replace</code> flag. If an entry with the
* same key already exists in the BTree, its value is returned.
*
* @param key Insert key
* @param value Insert value
* @param replace Set to true to replace an existing key-value pair.
* @return Existing value, if any.
*/
public V insert(final K key, final V value,
final boolean replace)
throws IOException {
if (key == null) {
throw new IllegalArgumentException("Argument 'key' is null");
}
if (value == null) {
throw new IllegalArgumentException("Argument 'value' is null");
}
try {
lock.writeLock().lock();
BTreeNode<K, V> rootNode = getRoot();
if (rootNode == null) {
// BTree is currently empty, create a new root BTreeNode
if (DEBUG) {
System.out.println("BTree.insert() new root BTreeNode");
}
rootNode = new BTreeNode<K, V>(this, key, value);
_root = rootNode._recid;
_height = 1;
_entries = 1;
_db.update(_recid, this);
modCount++;
//notifi listeners
for (RecordListener<K, V> l : recordListeners) {
l.recordInserted(key, value);
}
return null;
} else {
BTreeNode.InsertResult<K, V> insert = rootNode.insert(_height, key, value, replace);
boolean dirty = false;
if (insert._overflow != null) {
// current root node overflowed, we replace with a new root node
if (DEBUG) {
System.out.println("BTreeNode.insert() replace root BTreeNode due to overflow");
}
rootNode = new BTreeNode<K, V>(this, rootNode, insert._overflow);
_root = rootNode._recid;
_height += 1;
dirty = true;
}
if (insert._existing == null) {
_entries++;
modCount++;
dirty = true;
}
if (dirty) {
_db.update(_recid, this);
}
//notify listeners
for (RecordListener<K, V> l : recordListeners) {
if (insert._existing == null)
l.recordInserted(key, value);
else
l.recordUpdated(key, insert._existing, value);
}
// insert might have returned an existing value
V ret = insert._existing;
//zero out tuple and put it for reuse
insert._existing = null;
insert._overflow = null;
this.insertResultReuse = insert;
return ret;
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Remove an entry with the given key from the BTree.
*
* @param key Removal key
* @return Value associated with the key, or null if no entry with given
* key existed in the BTree.
*/
public V remove(K key)
throws IOException {
if (key == null) {
throw new IllegalArgumentException("Argument 'key' is null");
}
try {
lock.writeLock().lock();
BTreeNode<K, V> rootNode = getRoot();
if (rootNode == null) {
return null;
}
boolean dirty = false;
BTreeNode.RemoveResult<K, V> remove = rootNode.remove(_height, key);
if (remove._underflow && rootNode.isEmpty()) {
_height -= 1;
dirty = true;
_db.delete(_root);
if (_height == 0) {
_root = 0;
} else {
_root = rootNode.loadLastChildNode()._recid;
}
}
if (remove._value != null) {
_entries--;
modCount++;
dirty = true;
}
if (dirty) {
_db.update(_recid, this);
}
if (remove._value != null)
for (RecordListener<K, V> l : recordListeners)
l.recordRemoved(key, remove._value);
return remove._value;
} finally {
lock.writeLock().unlock();
}
}
/**
* Find the value associated with the given key.
*
* @param key Lookup key.
* @return Value associated with the key, or null if not found.
*/
public V get(K key)
throws IOException {
if (key == null) {
throw new IllegalArgumentException("Argument 'key' is null");
}
try {
lock.readLock().lock();
BTreeNode<K, V> rootNode = getRoot();
if (rootNode == null) {
return null;
}
return rootNode.findValue(_height, key);
} finally {
lock.readLock().unlock();
}
}
/**
* Find the value associated with the given key, or the entry immediately
* following this key in the ordered BTree.
*
* @param key Lookup key.
* @return Value associated with the key, or a greater entry, or null if no
* greater entry was found.
*/
public BTreeTuple<K, V> findGreaterOrEqual(K key)
throws IOException {
BTreeTuple<K, V> tuple;
BTreeTupleBrowser<K, V> browser;
if (key == null) {
// there can't be a key greater than or equal to "null"
// because null is considered an infinite key.
return null;
}
tuple = new BTreeTuple<K, V>(null, null);
browser = browse(key,true);
if (browser.getNext(tuple)) {
return tuple;
} else {
return null;
}
}
/**
* Get a browser initially positioned at the beginning of the BTree.
* <p><b>
* WARNING: If you make structural modifications to the BTree during
* browsing, you will get inconsistent browing results.
* </b>
*
* @return Browser positionned at the beginning of the BTree.
*/
@SuppressWarnings("unchecked")
public BTreeTupleBrowser<K, V> browse()
throws IOException {
try {
lock.readLock().lock();
BTreeNode<K, V> rootNode = getRoot();
if (rootNode == null) {
return EMPTY_BROWSER;
}
return rootNode.findFirst();
} finally {
lock.readLock().unlock();
}
}
/**
* Get a browser initially positioned just before the given key.
* <p><b>
* WARNING: <EFBFBD>If you make structural modifications to the BTree during
* browsing, you will get inconsistent browing results.
* </b>
*
* @param key Key used to position the browser. If null, the browser
* will be positionned after the last entry of the BTree.
* (Null is considered to be an "infinite" key)
* @return Browser positionned just before the given key.
*/
@SuppressWarnings("unchecked")
public BTreeTupleBrowser<K, V> browse(final K key, final boolean inclusive)
throws IOException {
try {
lock.readLock().lock();
BTreeNode<K, V> rootNode = getRoot();
if (rootNode == null) {
return EMPTY_BROWSER;
}
BTreeTupleBrowser<K, V> browser = rootNode.find(_height, key, inclusive);
return browser;
} finally {
lock.readLock().unlock();
}
}
/**
* Return the persistent record identifier of the BTree.
*/
public long getRecid() {
return _recid;
}
/**
* Return the root BTreeNode, or null if it doesn't exist.
*/
BTreeNode<K, V> getRoot()
throws IOException {
if (_root == 0) {
return null;
}
BTreeNode<K, V> root = _db.fetch(_root, _nodeSerializer);
if (root != null) {
root._recid = _root;
root._btree = this;
}
return root;
}
static BTree readExternal(DataInput in, Serialization ser)
throws IOException, ClassNotFoundException {
BTree tree = new BTree();
tree._db = ser.db;
tree._height = in.readInt();
tree._recid = in.readLong();
tree._root = in.readLong();
tree._entries = in.readLong();
tree.hasValues = in.readBoolean();
tree._comparator = (Comparator) ser.deserialize(in);
tree.keySerializer = (Serializer) ser.deserialize(in);
tree.valueSerializer = (Serializer) ser.deserialize(in);
return tree;
}
public void writeExternal(DataOutput out)
throws IOException {
out.writeInt(_height);
out.writeLong(_recid);
out.writeLong(_root);
out.writeLong(_entries);
out.writeBoolean(hasValues);
_db.defaultSerializer().serialize(out, _comparator);
_db.defaultSerializer().serialize(out, keySerializer);
_db.defaultSerializer().serialize(out, valueSerializer);
}
/**
* Copyes tree from one db to other, defragmenting it allong the way
* @param recid
* @param r1
* @param r2
* @throws IOException
*/
public static void defrag(long recid, DBStore r1, DBStore r2) throws IOException {
try {
byte[] data = r1.fetchRaw(recid);
r2.forceInsert(recid, data);
DataInput in = new DataInputOutput(data);
BTree t = (BTree) r1.defaultSerializer().deserialize(in);
t.loadValues = false;
t._db = r1;
t._nodeSerializer = new BTreeNode(t, false);
BTreeNode p = t.getRoot();
if (p != null) {
r2.forceInsert(t._root, r1.fetchRaw(t._root));
p.defrag(r1, r2);
}
} catch (ClassNotFoundException e) {
throw new IOError(e);
}
}
/**
* Browser returning no element.
*/
private static final BTreeTupleBrowser EMPTY_BROWSER = new BTreeTupleBrowser() {
public boolean getNext(BTreeTuple tuple) {
return false;
}
public boolean getPrevious(BTreeTuple tuple) {
return false;
}
public void remove(Object key) {
throw new IndexOutOfBoundsException();
}
};
/**
* add RecordListener which is notified about record changes
*
* @param listener
*/
public void addRecordListener(RecordListener<K, V> listener) {
recordListeners = Arrays.copyOf(recordListeners, recordListeners.length + 1);
recordListeners[recordListeners.length - 1] = listener;
}
/**
* remove RecordListener which is notified about record changes
*
* @param listener
*/
public void removeRecordListener(RecordListener<K, V> listener) {
List l = Arrays.asList(recordListeners);
l.remove(listener);
recordListeners = (RecordListener[]) l.toArray(new RecordListener[1]);
}
public DBAbstract getRecordManager() {
return _db;
}
public Comparator<K> getComparator() {
return _comparator;
}
/**
* Deletes all BTreeNodes in this BTree
*/
public void clear()
throws IOException {
try {
lock.writeLock().lock();
BTreeNode<K, V> rootNode = getRoot();
if (rootNode != null)
rootNode.delete();
_entries = 0;
modCount++;
} finally {
lock.writeLock().unlock();
}
}
/**
* Used for debugging and testing only. Populates the 'out' list with
* the recids of all child nodes in the BTree.
*
* @param out
* @throws IOException
*/
void dumpChildNodeRecIDs(List<Long> out) throws IOException {
BTreeNode<K, V> root = getRoot();
if (root != null) {
out.add(root._recid);
root.dumpChildNodeRecIDs(out, _height);
}
}
public boolean hasValues() {
return hasValues;
}
/**
* Browser to traverse a collection of tuples. The browser allows for
* forward and reverse order traversal.
*
*
*/
static interface BTreeTupleBrowser<K, V> {
/**
* Get the next tuple.
*
* @param tuple Tuple into which values are copied.
* @return True if values have been copied in tuple, or false if there is no next tuple.
*/
boolean getNext(BTree.BTreeTuple<K, V> tuple) throws IOException;
/**
* Get the previous tuple.
*
* @param tuple Tuple into which values are copied.
* @return True if values have been copied in tuple, or false if there is no previous tuple.
*/
boolean getPrevious(BTree.BTreeTuple<K, V> tuple) throws IOException;
/**
* Remove an entry with given key, and increases browsers expectedModCount
* This method is here to support 'ConcurrentModificationException' on Map interface.
*
* @param key
*/
void remove(K key) throws IOException;
}
/**
* Tuple consisting of a key-value pair.
*/
static final class BTreeTuple<K, V> {
K key;
V value;
BTreeTuple() {
// empty
}
BTreeTuple(K key, V value) {
this.key = key;
this.value = value;
}
}
}

View File

@ -0,0 +1,97 @@
package org.apache.jdbm;
import java.io.*;
/**
* An record lazily loaded from store.
* This is used in BTree/HTree to store big records outside of index tree
*
* @author Jan Kotek
*/
class BTreeLazyRecord<E> {
private E value = null;
private DBAbstract db;
private Serializer<E> serializer;
final long recid;
BTreeLazyRecord(DBAbstract db, long recid, Serializer<E> serializer) {
this.db = db;
this.recid = recid;
this.serializer = serializer;
}
E get() {
if (value != null) return value;
try {
value = db.fetch(recid, serializer);
} catch (IOException e) {
throw new IOError(e);
}
return value;
}
void delete() {
try {
db.delete(recid);
} catch (IOException e) {
throw new IOError(e);
}
value = null;
serializer = null;
db = null;
}
/**
* Serialier used to insert already serialized data into store
*/
static final Serializer FAKE_SERIALIZER = new Serializer() {
public void serialize(DataOutput out, Object obj) throws IOException {
byte[] data = (byte[]) obj;
out.write(data);
}
public Object deserialize(DataInput in) throws IOException, ClassNotFoundException {
throw new UnsupportedOperationException();
}
};
static Object fastDeser(DataInputOutput in, Serializer serializer, int expectedSize) throws IOException, ClassNotFoundException {
//we should propably copy data for deserialization into separate buffer and pass it to Serializer
//but to make it faster, Serializer will operate directly on top of buffer.
//and we check that it readed correct number of bytes.
int origAvail = in.available();
if (origAvail == 0)
throw new InternalError(); //is backed up by byte[] buffer, so there should be always avail bytes
Object ret = serializer.deserialize(in);
//check than valueSerializer did not read more bytes, if yes it readed bytes from next record
int readed = origAvail - in.available();
if (readed > expectedSize)
throw new IOException("Serializer readed more bytes than is record size.");
else if (readed != expectedSize) {
//deserializer did not readed all bytes, unussual but valid.
//Skip some to get into correct position
for (int ii = 0; ii < expectedSize - readed; ii++)
in.readUnsignedByte();
}
return ret;
}
/**
* if value in tree is serialized in more bytes, it is stored as separate record outside of tree
* This value must be always smaller than 250
*/
static final int MAX_INTREE_RECORD_SIZE = 32;
static {
if (MAX_INTREE_RECORD_SIZE > 250) throw new Error();
}
static final int NULL = 255;
static final int LAZY_RECORD = 254;
}

View File

@ -0,0 +1,611 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.IOError;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentNavigableMap;
/**
* Wrapper for <code>BTree</code> which implements <code>ConcurrentNavigableMap</code> interface
*
* @param <K> key type
* @param <V> value type
*
* @author Jan Kotek
*/
class BTreeMap<K, V> extends AbstractMap<K, V> implements ConcurrentNavigableMap<K, V> {
protected BTree<K, V> tree;
protected final K fromKey;
protected final K toKey;
protected final boolean readonly;
protected NavigableSet<K> keySet2;
private final boolean toInclusive;
private final boolean fromInclusive;
public BTreeMap(BTree<K, V> tree, boolean readonly) {
this(tree, readonly, null, false, null, false);
}
protected BTreeMap(BTree<K, V> tree, boolean readonly, K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {
this.tree = tree;
this.fromKey = fromKey;
this.fromInclusive = fromInclusive;
this.toKey = toKey;
this.toInclusive = toInclusive;
this.readonly = readonly;
}
@Override
public Set<Entry<K, V>> entrySet() {
return _entrySet;
}
private final Set<java.util.Map.Entry<K, V>> _entrySet = new AbstractSet<Entry<K, V>>() {
protected Entry<K, V> newEntry(K k, V v) {
return new SimpleEntry<K, V>(k, v) {
private static final long serialVersionUID = 978651696969194154L;
public V setValue(V arg0) {
BTreeMap.this.put(getKey(), arg0);
return super.setValue(arg0);
}
};
}
public boolean add(java.util.Map.Entry<K, V> e) {
if (readonly)
throw new UnsupportedOperationException("readonly");
try {
if (e.getKey() == null)
throw new NullPointerException("Can not add null key");
if (!inBounds(e.getKey()))
throw new IllegalArgumentException("key outside of bounds");
return tree.insert(e.getKey(), e.getValue(), true) == null;
} catch (IOException e1) {
throw new IOError(e1);
}
}
@SuppressWarnings("unchecked")
public boolean contains(Object o) {
if (o instanceof Entry) {
Entry<K, V> e = (java.util.Map.Entry<K, V>) o;
try {
if (!inBounds(e.getKey()))
return false;
if (e.getKey() != null && tree.get(e.getKey()) != null)
return true;
} catch (IOException e1) {
throw new IOError(e1);
}
}
return false;
}
public Iterator<java.util.Map.Entry<K, V>> iterator() {
try {
final BTree.BTreeTupleBrowser<K, V> br = fromKey == null ?
tree.browse() : tree.browse(fromKey, fromInclusive);
return new Iterator<Entry<K, V>>() {
private Entry<K, V> next;
private K lastKey;
void ensureNext() {
try {
BTree.BTreeTuple<K, V> t = new BTree.BTreeTuple<K, V>();
if (br.getNext(t) && inBounds(t.key))
next = newEntry(t.key, t.value);
else
next = null;
} catch (IOException e1) {
throw new IOError(e1);
}
}
{
ensureNext();
}
public boolean hasNext() {
return next != null;
}
public java.util.Map.Entry<K, V> next() {
if (next == null)
throw new NoSuchElementException();
Entry<K, V> ret = next;
lastKey = ret.getKey();
//move to next position
ensureNext();
return ret;
}
public void remove() {
if (readonly)
throw new UnsupportedOperationException("readonly");
if (lastKey == null)
throw new IllegalStateException();
try {
br.remove(lastKey);
lastKey = null;
} catch (IOException e1) {
throw new IOError(e1);
}
}
};
} catch (IOException e) {
throw new IOError(e);
}
}
@SuppressWarnings("unchecked")
public boolean remove(Object o) {
if (readonly)
throw new UnsupportedOperationException("readonly");
if (o instanceof Entry) {
Entry<K, V> e = (java.util.Map.Entry<K, V>) o;
try {
//check for nulls
if (e.getKey() == null || e.getValue() == null)
return false;
if (!inBounds(e.getKey()))
throw new IllegalArgumentException("out of bounds");
//get old value, must be same as item in entry
V v = get(e.getKey());
if (v == null || !e.getValue().equals(v))
return false;
V v2 = tree.remove(e.getKey());
return v2 != null;
} catch (IOException e1) {
throw new IOError(e1);
}
}
return false;
}
public int size() {
return BTreeMap.this.size();
}
public void clear(){
if(fromKey!=null || toKey!=null)
super.clear();
else
try {
tree.clear();
} catch (IOException e) {
throw new IOError(e);
}
}
};
public boolean inBounds(K e) {
if(fromKey == null && toKey == null)
return true;
Comparator comp = comparator();
if (comp == null) comp = Utils.COMPARABLE_COMPARATOR;
if(fromKey!=null){
final int compare = comp.compare(e, fromKey);
if(compare<0) return false;
if(!fromInclusive && compare == 0) return false;
}
if(toKey!=null){
final int compare = comp.compare(e, toKey);
if(compare>0)return false;
if(!toInclusive && compare == 0) return false;
}
return true;
}
@SuppressWarnings("unchecked")
@Override
public V get(Object key) {
try {
if (key == null)
return null;
if (!inBounds((K) key))
return null;
return tree.get((K) key);
} catch (ClassCastException e) {
return null;
} catch (IOException e) {
throw new IOError(e);
}
}
@SuppressWarnings("unchecked")
@Override
public V remove(Object key) {
if (readonly)
throw new UnsupportedOperationException("readonly");
try {
if (key == null || tree.get((K) key) == null)
return null;
if (!inBounds((K) key))
throw new IllegalArgumentException("out of bounds");
return tree.remove((K) key);
} catch (ClassCastException e) {
return null;
} catch (IOException e) {
throw new IOError(e);
}
}
public V put(K key, V value) {
if (readonly)
throw new UnsupportedOperationException("readonly");
try {
if (key == null || value == null)
throw new NullPointerException("Null key or value");
if (!inBounds(key))
throw new IllegalArgumentException("out of bounds");
return tree.insert(key, value, true);
} catch (IOException e) {
throw new IOError(e);
}
}
public void clear(){
entrySet().clear();
}
@SuppressWarnings("unchecked")
@Override
public boolean containsKey(Object key) {
if (key == null)
return false;
try {
if (!inBounds((K) key))
return false;
V v = tree.get((K) key);
return v != null;
} catch (IOException e) {
throw new IOError(e);
} catch (ClassCastException e) {
return false;
}
}
public Comparator<? super K> comparator() {
return tree._comparator;
}
public K firstKey() {
if (isEmpty())
return null;
try {
BTree.BTreeTupleBrowser<K, V> b = fromKey == null ? tree.browse() : tree.browse(fromKey,fromInclusive);
BTree.BTreeTuple<K, V> t = new BTree.BTreeTuple<K, V>();
b.getNext(t);
return t.key;
} catch (IOException e) {
throw new IOError(e);
}
}
public K lastKey() {
if (isEmpty())
return null;
try {
BTree.BTreeTupleBrowser<K, V> b = toKey == null ? tree.browse(null,true) : tree.browse(toKey,false);
BTree.BTreeTuple<K, V> t = new BTree.BTreeTuple<K, V>();
b.getPrevious(t);
if(!toInclusive && toKey!=null){
//make sure we wont return last key
Comparator c = comparator();
if(c==null) c=Utils.COMPARABLE_COMPARATOR;
if(c.compare(t.key,toKey)==0)
b.getPrevious(t);
}
return t.key;
} catch (IOException e) {
throw new IOError(e);
}
}
public ConcurrentNavigableMap<K, V> headMap(K toKey2, boolean inclusive) {
K toKey3 = Utils.min(this.toKey,toKey2,comparator());
boolean inclusive2 = toKey3 == toKey? toInclusive : inclusive;
return new BTreeMap<K, V>(tree, readonly, this.fromKey, this.fromInclusive, toKey3, inclusive2);
}
public ConcurrentNavigableMap<K, V> headMap(K toKey) {
return headMap(toKey,false);
}
public Entry<K, V> lowerEntry(K key) {
K k = lowerKey(key);
return k==null? null : new SimpleEntry<K, V>(k,get(k));
}
public K lowerKey(K key) {
if (isEmpty())
return null;
K key2 = Utils.min(key,toKey,comparator());
try {
BTree.BTreeTupleBrowser<K, V> b = tree.browse(key2,true) ;
BTree.BTreeTuple<K, V> t = new BTree.BTreeTuple<K, V>();
b.getPrevious(t);
return t.key;
} catch (IOException e) {
throw new IOError(e);
}
}
public Entry<K, V> floorEntry(K key) {
K k = floorKey(key);
return k==null? null : new SimpleEntry<K, V>(k,get(k));
}
public K floorKey(K key) {
if (isEmpty())
return null;
K key2 = Utils.max(key,fromKey,comparator());
try {
BTree.BTreeTupleBrowser<K, V> b = tree.browse(key2,true) ;
BTree.BTreeTuple<K, V> t = new BTree.BTreeTuple<K, V>();
b.getNext(t);
Comparator comp = comparator();
if (comp == null) comp = Utils.COMPARABLE_COMPARATOR;
if(comp.compare(t.key,key2) == 0)
return t.key;
b.getPrevious(t);
b.getPrevious(t);
return t.key;
} catch (IOException e) {
throw new IOError(e);
}
}
public Entry<K, V> ceilingEntry(K key) {
K k = ceilingKey(key);
return k==null? null : new SimpleEntry<K, V>(k,get(k));
}
public K ceilingKey(K key) {
if (isEmpty())
return null;
K key2 = Utils.min(key,toKey,comparator());
try {
BTree.BTreeTupleBrowser<K, V> b = tree.browse(key2,true) ;
BTree.BTreeTuple<K, V> t = new BTree.BTreeTuple<K, V>();
b.getNext(t);
return t.key;
} catch (IOException e) {
throw new IOError(e);
}
}
public Entry<K, V> higherEntry(K key) {
K k = higherKey(key);
return k==null? null : new SimpleEntry<K, V>(k,get(k));
}
public K higherKey(K key) {
if (isEmpty())
return null;
K key2 = Utils.max(key,fromKey,comparator());
try {
BTree.BTreeTupleBrowser<K, V> b = tree.browse(key2,false) ;
BTree.BTreeTuple<K, V> t = new BTree.BTreeTuple<K, V>();
b.getNext(t);
return t.key;
} catch (IOException e) {
throw new IOError(e);
}
}
public Entry<K, V> firstEntry() {
K k = firstKey();
return k==null? null : new SimpleEntry<K, V>(k,get(k));
}
public Entry<K, V> lastEntry() {
K k = lastKey();
return k==null? null : new SimpleEntry<K, V>(k,get(k));
}
public Entry<K, V> pollFirstEntry() {
Entry<K,V> first = firstEntry();
if(first!=null)
remove(first.getKey());
return first;
}
public Entry<K, V> pollLastEntry() {
Entry<K,V> last = lastEntry();
if(last!=null)
remove(last.getKey());
return last;
}
public ConcurrentNavigableMap<K, V> descendingMap() {
throw new UnsupportedOperationException("not implemented yet");
//TODO implement descending (reverse order) map
}
public NavigableSet<K> keySet() {
return navigableKeySet();
}
public NavigableSet<K> navigableKeySet() {
if(keySet2 == null)
keySet2 = new BTreeSet<K>((BTreeMap<K,Object>) this);
return keySet2;
}
public NavigableSet<K> descendingKeySet() {
return descendingMap().navigableKeySet();
}
public ConcurrentNavigableMap<K, V> tailMap(K fromKey) {
return tailMap(fromKey,true);
}
public ConcurrentNavigableMap<K, V> tailMap(K fromKey2, boolean inclusive) {
K fromKey3 = Utils.max(this.fromKey,fromKey2,comparator());
boolean inclusive2 = fromKey3 == toKey? toInclusive : inclusive;
return new BTreeMap<K, V>(tree, readonly, fromKey3, inclusive2, toKey, toInclusive);
}
public ConcurrentNavigableMap<K, V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {
Comparator comp = comparator();
if (comp == null) comp = Utils.COMPARABLE_COMPARATOR;
if (comp.compare(fromKey, toKey) > 0)
throw new IllegalArgumentException("fromKey is bigger then toKey");
return new BTreeMap<K, V>(tree, readonly, fromKey, fromInclusive, toKey, toInclusive);
}
public ConcurrentNavigableMap<K, V> subMap(K fromKey, K toKey) {
return subMap(fromKey,true,toKey,false);
}
public BTree<K, V> getTree() {
return tree;
}
public void addRecordListener(RecordListener<K, V> listener) {
tree.addRecordListener(listener);
}
public DBAbstract getRecordManager() {
return tree.getRecordManager();
}
public void removeRecordListener(RecordListener<K, V> listener) {
tree.removeRecordListener(listener);
}
public int size() {
if (fromKey == null && toKey == null)
return (int) tree._entries; //use fast counter on tree if Map has no bounds
else {
//had to count items in iterator
Iterator iter = keySet().iterator();
int counter = 0;
while (iter.hasNext()) {
iter.next();
counter++;
}
return counter;
}
}
public V putIfAbsent(K key, V value) {
tree.lock.writeLock().lock();
try{
if (!containsKey(key))
return put(key, value);
else
return get(key);
}finally {
tree.lock.writeLock().unlock();
}
}
public boolean remove(Object key, Object value) {
tree.lock.writeLock().lock();
try{
if (containsKey(key) && get(key).equals(value)) {
remove(key);
return true;
} else return false;
}finally {
tree.lock.writeLock().unlock();
}
}
public boolean replace(K key, V oldValue, V newValue) {
tree.lock.writeLock().lock();
try{
if (containsKey(key) && get(key).equals(oldValue)) {
put(key, newValue);
return true;
} else return false;
}finally {
tree.lock.writeLock().unlock();
}
}
public V replace(K key, V value) {
tree.lock.writeLock().lock();
try{
if (containsKey(key)) {
return put(key, value);
} else return null;
}finally {
tree.lock.writeLock().unlock();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jdbm;
import java.util.*;
/**
* Wrapper class for <code>>SortedMap</code> to implement <code>>NavigableSet</code>
* <p/>
* This code originally comes from Apache Harmony, was adapted by Jan Kotek for JDBM
*/
class BTreeSet<E> extends AbstractSet<E> implements NavigableSet<E> {
/**
* use keyset from this map
*/
final BTreeMap<E, Object> map;
BTreeSet(BTreeMap<E, Object> map) {
this.map = map;
}
public boolean add(E object) {
return map.put(object, Utils.EMPTY_STRING) == null;
}
public boolean addAll(Collection<? extends E> collection) {
return super.addAll(collection);
}
public void clear() {
map.clear();
}
public Comparator<? super E> comparator() {
return map.comparator();
}
public boolean contains(Object object) {
return map.containsKey(object);
}
public boolean isEmpty() {
return map.isEmpty();
}
public E lower(E e) {
return map.lowerKey(e);
}
public E floor(E e) {
return map.floorKey(e);
}
public E ceiling(E e) {
return map.ceilingKey(e);
}
public E higher(E e) {
return map.higherKey(e);
}
public E pollFirst() {
Map.Entry<E,Object> e = map.pollFirstEntry();
return e!=null? e.getKey():null;
}
public E pollLast() {
Map.Entry<E,Object> e = map.pollLastEntry();
return e!=null? e.getKey():null;
}
public Iterator<E> iterator() {
final Iterator<Map.Entry<E,Object>> iter = map.entrySet().iterator();
return new Iterator<E>() {
public boolean hasNext() {
return iter.hasNext();
}
public E next() {
Map.Entry<E,Object> e = iter.next();
return e!=null?e.getKey():null;
}
public void remove() {
iter.remove();
}
};
}
public NavigableSet<E> descendingSet() {
return map.descendingKeySet();
}
public Iterator<E> descendingIterator() {
return map.descendingKeySet().iterator();
}
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) {
return map.subMap(fromElement,fromInclusive,toElement,toInclusive).navigableKeySet();
}
public NavigableSet<E> headSet(E toElement, boolean inclusive) {
return map.headMap(toElement,inclusive).navigableKeySet();
}
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
return map.tailMap(fromElement,inclusive).navigableKeySet();
}
public boolean remove(Object object) {
return map.remove(object) != null;
}
public int size() {
return map.size();
}
public E first() {
return map.firstKey();
}
public E last() {
return map.lastKey();
}
public SortedSet<E> subSet(E start, E end) {
Comparator<? super E> c = map.comparator();
int compare = (c == null) ? ((Comparable<E>) start).compareTo(end) : c
.compare(start, end);
if (compare <= 0) {
return new BTreeSet<E>((BTreeMap<E,Object>) map.subMap(start, true,end,false));
}
throw new IllegalArgumentException();
}
public SortedSet<E> headSet(E end) {
// Check for errors
Comparator<? super E> c = map.comparator();
if (c == null) {
((Comparable<E>) end).compareTo(end);
} else {
c.compare(end, end);
}
return new BTreeSet<E>((BTreeMap<E,Object>) map.headMap(end,false));
}
public SortedSet<E> tailSet(E start) {
// Check for errors
Comparator<? super E> c = map.comparator();
if (c == null) {
((Comparable<E>) start).compareTo(start);
} else {
c.compare(start, start);
}
return new BTreeSet<E>((BTreeMap<E,Object>) map.tailMap(start,true));
}
}

View File

@ -0,0 +1,173 @@
package org.apache.jdbm;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentNavigableMap;
/**
* Database is root class for creating and loading persistent collections. It also contains
* transaction operations.
* //TODO just write some readme
* <p/>
*
* @author Jan Kotek
* @author Alex Boisvert
* @author Cees de Groot
*/
public interface DB {
/**
* Closes the DB and release resources.
* DB can not be used after it was closed
*/
void close();
/** @return true if db was already closed */
boolean isClosed();
/**
* Clear cache and remove all entries it contains.
* This may be useful for some Garbage Collection when reference cache is used.
*/
void clearCache();
/**
* Defragments storage so it consumes less space.
* It basically copyes all records into different store and then renames it, replacing original store.
* <p/>
* Defrag has two steps: In first collections are rearranged, so records in collection are close to each other,
* and read speed is improved. In second step all records are sequentially transferred, reclaiming all unused space.
* First step is optinal and may slow down defragmentation significantly as ut requires many random-access reads.
* Second step reads and writes data sequentially and is very fast, comparable to copying files to new location.
*
* <p/>
* This commits any uncommited data. Defrag also requires free space, as store is basically recreated at new location.
*
* @param sortCollections if collection records should be rearranged during defragment, this takes some extra time
*/
void defrag(boolean sortCollections);
/**
* Commit (make persistent) all changes since beginning of transaction.
* JDBM supports only single transaction.
*/
void commit();
/**
* Rollback (cancel) all changes since beginning of transaction.
* JDBM supports only single transaction.
* This operations affects all maps created or loaded by this DB.
*/
void rollback();
/**
* This calculates some database statistics such as collection sizes and record distributions.
* Can be useful for performance optimalisations and trouble shuting.
* This method can run for very long time.
*
* @return statistics contained in string
*/
String calculateStatistics();
/**
* Copy database content into ZIP file
* @param zipFile
*/
void copyToZip(String zipFile);
/**
* Get a <code>Map</code> which was already created and saved in DB.
* This map uses disk based H*Tree and should have similar performance
* as <code>HashMap</code>.
*
* @param name of hash map
*
* @return map
*/
<K, V> ConcurrentMap<K, V> getHashMap(String name);
/**
* Creates Map which persists data into DB.
*
* @param name record name
* @return
*/
<K, V> ConcurrentMap<K, V> createHashMap(String name);
/**
* Creates Hash Map which persists data into DB.
* Map will use custom serializers for Keys and Values.
* Leave keySerializer null to use default serializer for keys
*
* @param <K> Key type
* @param <V> Value type
* @param name record name
* @param keySerializer serializer to be used for Keys, leave null to use default serializer
* @param valueSerializer serializer to be used for Values
* @return
*/
<K, V> ConcurrentMap<K, V> createHashMap(String name, Serializer<K> keySerializer, Serializer<V> valueSerializer);
<K> Set<K> createHashSet(String name);
<K> Set<K> getHashSet(String name);
<K> Set<K> createHashSet(String name, Serializer<K> keySerializer);
<K, V> ConcurrentNavigableMap<K, V> getTreeMap(String name);
/**
* Create TreeMap which persists data into DB.
*
* @param <K> Key type
* @param <V> Value type
* @param name record name
* @return
*/
<K extends Comparable, V> NavigableMap<K, V> createTreeMap(String name);
/**
* Creates TreeMap which persists data into DB.
*
* @param <K> Key type
* @param <V> Value type
* @param name record name
* @param keyComparator Comparator used to sort keys
* @param keySerializer Serializer used for keys. This may reduce disk space usage *
* @param valueSerializer Serializer used for values. This may reduce disk space usage
* @return
*/
<K, V> ConcurrentNavigableMap<K, V> createTreeMap(String name,
Comparator<K> keyComparator, Serializer<K> keySerializer, Serializer<V> valueSerializer);
<K> NavigableSet<K> getTreeSet(String name);
<K> NavigableSet<K> createTreeSet(String name);
<K> NavigableSet<K> createTreeSet(String name, Comparator<K> keyComparator, Serializer<K> keySerializer);
<K> List<K> createLinkedList(String name);
<K> List<K> createLinkedList(String name, Serializer<K> serializer);
<K> List<K> getLinkedList(String name);
/** returns unmodifiable map which contains all collection names and collections thenselfs*/
Map<String,Object> getCollections();
/** completely remove collection from store*/
void deleteCollection(String name);
/** Java Collections returns their size as int. This may not be enought for JDBM collections.
* This method returns number of elements in JDBM collection as long.
*
* @param collection created by JDBM
* @return number of elements in collection as long
*/
long collectionSize(Object collection);
}

View File

@ -0,0 +1,590 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOError;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentNavigableMap;
/**
* An abstract class implementing most of DB.
* It also has some JDBM package protected stuff (getNamedRecord)
*/
abstract class DBAbstract implements DB {
/**
* Reserved slot for name directory recid.
*/
static final byte NAME_DIRECTORY_ROOT = 0;
/**
* Reserved slot for version number
*/
static final byte STORE_VERSION_NUMBER_ROOT = 1;
/**
* Reserved slot for recid where Serial class info is stored
*
* NOTE when introducing more roots, do not forget to update defrag
*/
static final byte SERIAL_CLASS_INFO_RECID_ROOT = 2;
/** to prevent double instances of the same collection, we use weak value map
*
* //TODO what to do when there is rollback?
* //TODO clear on close
*/
final private Map<String,WeakReference<Object>> collections = new HashMap<String,WeakReference<Object>>();
/**
* Inserts a new record using a custom serializer.
*
* @param obj the object for the new record.
* @param serializer a custom serializer
* @return the rowid for the new record.
* @throws java.io.IOException when one of the underlying I/O operations fails.
*/
abstract <A> long insert(A obj, Serializer<A> serializer,boolean disableCache) throws IOException;
/**
* Deletes a record.
*
* @param recid the rowid for the record that should be deleted.
* @throws java.io.IOException when one of the underlying I/O operations fails.
*/
abstract void delete(long recid) throws IOException;
/**
* Updates a record using a custom serializer.
* If given recid does not exist, IOException will be thrown before/during commit (cache).
*
* @param recid the recid for the record that is to be updated.
* @param obj the new object for the record.
* @param serializer a custom serializer
* @throws java.io.IOException when one of the underlying I/O operations fails
*/
abstract <A> void update(long recid, A obj, Serializer<A> serializer)
throws IOException;
/**
* Fetches a record using a custom serializer.
*
* @param recid the recid for the record that must be fetched.
* @param serializer a custom serializer
* @return the object contained in the record, null if given recid does not exist
* @throws java.io.IOException when one of the underlying I/O operations fails.
*/
abstract <A> A fetch(long recid, Serializer<A> serializer)
throws IOException;
/**
* Fetches a record using a custom serializer and optionaly disabled cache
*
* @param recid the recid for the record that must be fetched.
* @param serializer a custom serializer
* @param disableCache true to disable any caching mechanism
* @return the object contained in the record, null if given recid does not exist
* @throws java.io.IOException when one of the underlying I/O operations fails.
*/
abstract <A> A fetch(long recid, Serializer<A> serializer, boolean disableCache)
throws IOException;
public long insert(Object obj) throws IOException {
return insert(obj, defaultSerializer(),false);
}
public void update(long recid, Object obj) throws IOException {
update(recid, obj, defaultSerializer());
}
synchronized public <A> A fetch(long recid) throws IOException {
return (A) fetch(recid, defaultSerializer());
}
synchronized public <K, V> ConcurrentMap<K, V> getHashMap(String name) {
Object o = getCollectionInstance(name);
if(o!=null)
return (ConcurrentMap<K, V>) o;
try {
long recid = getNamedObject(name);
if(recid == 0) return null;
HTree tree = fetch(recid);
tree.setPersistenceContext(this);
if(!tree.hasValues()){
throw new ClassCastException("HashSet is not HashMap");
}
collections.put(name,new WeakReference<Object>(tree));
return tree;
} catch (IOException e) {
throw new IOError(e);
}
}
synchronized public <K, V> ConcurrentMap<K, V> createHashMap(String name) {
return createHashMap(name, null, null);
}
public synchronized <K, V> ConcurrentMap<K, V> createHashMap(String name, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
try {
assertNameNotExist(name);
HTree<K, V> tree = new HTree(this, keySerializer, valueSerializer,true);
long recid = insert(tree);
setNamedObject(name, recid);
collections.put(name,new WeakReference<Object>(tree));
return tree;
} catch (IOException e) {
throw new IOError(e);
}
}
public synchronized <K> Set<K> getHashSet(String name) {
Object o = getCollectionInstance(name);
if(o!=null)
return (Set<K>) o;
try {
long recid = getNamedObject(name);
if(recid == 0) return null;
HTree tree = fetch(recid);
tree.setPersistenceContext(this);
if(tree.hasValues()){
throw new ClassCastException("HashMap is not HashSet");
}
Set<K> ret = new HTreeSet(tree);
collections.put(name,new WeakReference<Object>(ret));
return ret;
} catch (IOException e) {
throw new IOError(e);
}
}
public synchronized <K> Set<K> createHashSet(String name) {
return createHashSet(name, null);
}
public synchronized <K> Set<K> createHashSet(String name, Serializer<K> keySerializer) {
try {
assertNameNotExist(name);
HTree<K, Object> tree = new HTree(this, keySerializer, null,false);
long recid = insert(tree);
setNamedObject(name, recid);
Set<K> ret = new HTreeSet<K>(tree);
collections.put(name,new WeakReference<Object>(ret));
return ret;
} catch (IOException e) {
throw new IOError(e);
}
}
synchronized public <K, V> ConcurrentNavigableMap<K, V> getTreeMap(String name) {
Object o = getCollectionInstance(name);
if(o!=null)
return (ConcurrentNavigableMap<K, V> ) o;
try {
long recid = getNamedObject(name);
if(recid == 0) return null;
BTree t = BTree.<K, V>load(this, recid);
if(!t.hasValues())
throw new ClassCastException("TreeSet is not TreeMap");
ConcurrentNavigableMap<K,V> ret = new BTreeMap<K, V>(t,false); //TODO put readonly flag here
collections.put(name,new WeakReference<Object>(ret));
return ret;
} catch (IOException e) {
throw new IOError(e);
}
}
synchronized public <K extends Comparable, V> ConcurrentNavigableMap<K, V> createTreeMap(String name) {
return createTreeMap(name, null, null, null);
}
public synchronized <K, V> ConcurrentNavigableMap<K, V> createTreeMap(String name,
Comparator<K> keyComparator,
Serializer<K> keySerializer,
Serializer<V> valueSerializer) {
try {
assertNameNotExist(name);
BTree<K, V> tree = BTree.createInstance(this, keyComparator, keySerializer, valueSerializer,true);
setNamedObject(name, tree.getRecid());
ConcurrentNavigableMap<K,V> ret = new BTreeMap<K, V>(tree,false); //TODO put readonly flag here
collections.put(name,new WeakReference<Object>(ret));
return ret;
} catch (IOException e) {
throw new IOError(e);
}
}
public synchronized <K> NavigableSet<K> getTreeSet(String name) {
Object o = getCollectionInstance(name);
if(o!=null)
return (NavigableSet<K> ) o;
try {
long recid = getNamedObject(name);
if(recid == 0) return null;
BTree t = BTree.<K, Object>load(this, recid);
if(t.hasValues())
throw new ClassCastException("TreeMap is not TreeSet");
BTreeSet<K> ret = new BTreeSet<K>(new BTreeMap(t,false));
collections.put(name,new WeakReference<Object>(ret));
return ret;
} catch (IOException e) {
throw new IOError(e);
}
}
public synchronized <K> NavigableSet<K> createTreeSet(String name) {
return createTreeSet(name, null, null);
}
public synchronized <K> NavigableSet<K> createTreeSet(String name, Comparator<K> keyComparator, Serializer<K> keySerializer) {
try {
assertNameNotExist(name);
BTree<K, Object> tree = BTree.createInstance(this, keyComparator, keySerializer, null,false);
setNamedObject(name, tree.getRecid());
BTreeSet<K> ret = new BTreeSet<K>(new BTreeMap(tree,false));
collections.put(name,new WeakReference<Object>(ret));
return ret;
} catch (IOException e) {
throw new IOError(e);
}
}
synchronized public <K> List<K> createLinkedList(String name) {
return createLinkedList(name, null);
}
synchronized public <K> List<K> createLinkedList(String name, Serializer<K> serializer) {
try {
assertNameNotExist(name);
//allocate record and overwrite it
LinkedList2<K> list = new LinkedList2<K>(this, serializer);
long recid = insert(list);
setNamedObject(name, recid);
collections.put(name,new WeakReference<Object>(list));
return list;
} catch (IOException e) {
throw new IOError(e);
}
}
synchronized public <K> List<K> getLinkedList(String name) {
Object o = getCollectionInstance(name);
if(o!=null)
return (List<K> ) o;
try {
long recid = getNamedObject(name);
if(recid == 0) return null;
LinkedList2<K> list = (LinkedList2<K>) fetch(recid);
list.setPersistenceContext(this);
collections.put(name,new WeakReference<Object>(list));
return list;
} catch (IOException e) {
throw new IOError(e);
}
}
private synchronized Object getCollectionInstance(String name){
WeakReference ref = collections.get(name);
if(ref==null)return null;
Object o = ref.get();
if(o != null) return o;
//already GCed
collections.remove(name);
return null;
}
private void assertNameNotExist(String name) throws IOException {
if (getNamedObject(name) != 0)
throw new IllegalArgumentException("Object with name '" + name + "' already exists");
}
/**
* Obtain the record id of a named object. Returns 0 if named object
* doesn't exist.
* Named objects are used to store Map views and other well known objects.
*/
synchronized protected long getNamedObject(String name) throws IOException{
long nameDirectory_recid = getRoot(NAME_DIRECTORY_ROOT);
if(nameDirectory_recid == 0){
return 0;
}
HTree<String,Long> m = fetch(nameDirectory_recid);
Long res = m.get(name);
if(res == null)
return 0;
return res;
}
/**
* Set the record id of a named object.
* Named objects are used to store Map views and other well known objects.
*/
synchronized protected void setNamedObject(String name, long recid) throws IOException{
long nameDirectory_recid = getRoot(NAME_DIRECTORY_ROOT);
HTree<String,Long> m = null;
if(nameDirectory_recid == 0){
//does not exists, create it
m = new HTree<String, Long>(this,null,null,true);
nameDirectory_recid = insert(m);
setRoot(NAME_DIRECTORY_ROOT,nameDirectory_recid);
}else{
//fetch it
m = fetch(nameDirectory_recid);
}
m.put(name,recid);
}
synchronized public Map<String,Object> getCollections(){
try{
Map<String,Object> ret = new LinkedHashMap<String, Object>();
long nameDirectory_recid = getRoot(NAME_DIRECTORY_ROOT);
if(nameDirectory_recid==0)
return ret;
HTree<String,Long> m = fetch(nameDirectory_recid);
for(Map.Entry<String,Long> e:m.entrySet()){
Object o = fetch(e.getValue());
if(o instanceof BTree){
if(((BTree) o).hasValues)
o = getTreeMap(e.getKey());
else
o = getTreeSet(e.getKey());
}
else if( o instanceof HTree){
if(((HTree) o).hasValues)
o = getHashMap(e.getKey());
else
o = getHashSet(e.getKey());
}
ret.put(e.getKey(), o);
}
return Collections.unmodifiableMap(ret);
}catch(IOException e){
throw new IOError(e);
}
}
synchronized public void deleteCollection(String name){
try{
long nameDirectory_recid = getRoot(NAME_DIRECTORY_ROOT);
if(nameDirectory_recid==0)
throw new IOException("Collection not found");
HTree<String,Long> dir = fetch(nameDirectory_recid);
Long recid = dir.get(name);
if(recid == null) throw new IOException("Collection not found");
Object o = fetch(recid);
//we can not use O instance since it is not correctly initialized
if(o instanceof LinkedList2){
LinkedList2 l = (LinkedList2) o;
l.clear();
delete(l.rootRecid);
}else if(o instanceof BTree){
((BTree) o).clear();
} else if( o instanceof HTree){
HTree t = (HTree) o;
t.clear();
HTreeDirectory n = (HTreeDirectory) fetch(t.rootRecid,t.SERIALIZER);
n.deleteAllChildren();
delete(t.rootRecid);
}else{
throw new InternalError("unknown collection type: "+(o==null?null:o.getClass()));
}
delete(recid);
collections.remove(name);
dir.remove(name);
}catch(IOException e){
throw new IOError(e);
}
}
/** we need to set reference to this DB instance, so serializer needs to be here*/
final Serializer<Serialization> defaultSerializationSerializer = new Serializer<Serialization>(){
public void serialize(DataOutput out, Serialization obj) throws IOException {
LongPacker.packLong(out,obj.serialClassInfoRecid);
SerialClassInfo.serializer.serialize(out,obj.registered);
}
public Serialization deserialize(DataInput in) throws IOException, ClassNotFoundException {
final long recid = LongPacker.unpackLong(in);
final ArrayList<SerialClassInfo.ClassInfo> classes = SerialClassInfo.serializer.deserialize(in);
return new Serialization(DBAbstract.this,recid,classes);
}
};
public synchronized Serializer defaultSerializer() {
try{
long serialClassInfoRecid = getRoot(SERIAL_CLASS_INFO_RECID_ROOT);
if (serialClassInfoRecid == 0) {
//allocate new recid
serialClassInfoRecid = insert(null,Utils.NULL_SERIALIZER,false);
//and insert new serializer
Serialization ser = new Serialization(this,serialClassInfoRecid,new ArrayList<SerialClassInfo.ClassInfo>());
update(serialClassInfoRecid,ser, defaultSerializationSerializer);
setRoot(SERIAL_CLASS_INFO_RECID_ROOT, serialClassInfoRecid);
return ser;
}else{
return fetch(serialClassInfoRecid,defaultSerializationSerializer);
}
} catch (IOException e) {
throw new IOError(e);
}
}
final protected void checkNotClosed(){
if(isClosed()) throw new IllegalStateException("db was closed");
}
protected abstract void setRoot(byte root, long recid);
protected abstract long getRoot(byte root);
synchronized public long collectionSize(Object collection){
if(collection instanceof BTreeMap){
BTreeMap t = (BTreeMap) collection;
if(t.fromKey!=null|| t.toKey!=null) throw new IllegalArgumentException("collectionSize does not work on BTree submap");
return t.tree._entries;
}else if(collection instanceof HTree){
return ((HTree)collection).getRoot().size;
}else if(collection instanceof HTreeSet){
return collectionSize(((HTreeSet) collection).map);
}else if(collection instanceof BTreeSet){
return collectionSize(((BTreeSet) collection).map);
}else if(collection instanceof LinkedList2){
return ((LinkedList2)collection).getRoot().size;
}else{
throw new IllegalArgumentException("Not JDBM collection");
}
}
void addShutdownHook(){
if(shutdownCloseThread!=null){
shutdownCloseThread = new ShutdownCloseThread();
Runtime.getRuntime().addShutdownHook(shutdownCloseThread);
}
}
public void close(){
if(shutdownCloseThread!=null){
Runtime.getRuntime().removeShutdownHook(shutdownCloseThread);
shutdownCloseThread.dbToClose = null;
shutdownCloseThread = null;
}
}
ShutdownCloseThread shutdownCloseThread = null;
private static class ShutdownCloseThread extends Thread{
DBAbstract dbToClose = null;
ShutdownCloseThread(){
super("JDBM shutdown");
}
public void run(){
if(dbToClose!=null && !dbToClose.isClosed()){
dbToClose.shutdownCloseThread = null;
dbToClose.close();
}
}
}
synchronized public void rollback() {
try {
for(WeakReference<Object> o:collections.values()){
Object c = o.get();
if(c != null && c instanceof BTreeMap){
//reload tree
BTreeMap m = (BTreeMap) c;
m.tree = fetch(m.tree.getRecid());
}
if(c != null && c instanceof BTreeSet){
//reload tree
BTreeSet m = (BTreeSet) c;
m.map.tree = fetch(m.map.tree.getRecid());
}
}
} catch (IOException e) {
throw new IOError(e);
}
}
}

View File

@ -0,0 +1,162 @@
package org.apache.jdbm;
import javax.crypto.Cipher;
import java.io.IOError;
import java.io.IOException;
import java.util.Comparator;
import java.util.Iterator;
/**
* Abstract class with common cache functionality
*/
abstract class DBCache extends DBStore{
static final int NUM_OF_DIRTY_RECORDS_BEFORE_AUTOCOMIT = 1024;
static final byte NONE = 1;
static final byte MRU = 2;
static final byte WEAK = 3;
static final byte SOFT = 4;
static final byte HARD = 5;
static final class DirtyCacheEntry {
long _recid; //TODO recid is already part of _hashDirties, so this field could be removed to save memory
Object _obj;
Serializer _serializer;
}
/**
* Dirty status of _hash CacheEntry Values
*/
final protected LongHashMap<DirtyCacheEntry> _hashDirties = new LongHashMap<DirtyCacheEntry>();
private Serializer cachedDefaultSerializer = null;
/**
* Construct a CacheRecordManager wrapping another DB and
* using a given cache policy.
*/
public DBCache(String filename, boolean readonly, boolean transactionDisabled,
Cipher cipherIn, Cipher cipherOut, boolean useRandomAccessFile,
boolean deleteFilesAfterClose,boolean lockingDisabled){
super(filename, readonly, transactionDisabled,
cipherIn, cipherOut, useRandomAccessFile,
deleteFilesAfterClose,lockingDisabled);
}
@Override
public synchronized Serializer defaultSerializer(){
if(cachedDefaultSerializer==null)
cachedDefaultSerializer = super.defaultSerializer();
return cachedDefaultSerializer;
}
@Override
boolean needsAutoCommit() {
return super.needsAutoCommit()||
(transactionsDisabled && !commitInProgress && _hashDirties.size() > NUM_OF_DIRTY_RECORDS_BEFORE_AUTOCOMIT);
}
public synchronized <A> long insert(final A obj, final Serializer<A> serializer, final boolean disableCache)
throws IOException {
checkNotClosed();
if(super.needsAutoCommit())
commit();
if(disableCache)
return super.insert(obj, serializer, disableCache);
//prealocate recid so we have something to return
final long recid = super.insert(PREALOCATE_OBJ, null, disableCache);
// super.update(recid, obj,serializer);
// return super.insert(obj,serializer,disableCache);
//and create new dirty record for future update
final DirtyCacheEntry e = new DirtyCacheEntry();
e._recid = recid;
e._obj = obj;
e._serializer = serializer;
_hashDirties.put(recid,e);
return recid;
}
public synchronized void commit() {
try{
commitInProgress = true;
updateCacheEntries();
super.commit();
}finally {
commitInProgress = false;
}
}
public synchronized void rollback(){
cachedDefaultSerializer = null;
_hashDirties.clear();
super.rollback();
}
private static final Comparator<DirtyCacheEntry> DIRTY_COMPARATOR = new Comparator<DirtyCacheEntry>() {
final public int compare(DirtyCacheEntry o1, DirtyCacheEntry o2) {
return (int) (o1._recid - o2._recid);
}
};
/**
* Update all dirty cache objects to the underlying DB.
*/
protected void updateCacheEntries() {
try {
synchronized(_hashDirties){
while(!_hashDirties.isEmpty()){
//make defensive copy of values as _db.update() may trigger changes in db
//and this would modify dirties again
DirtyCacheEntry[] vals = new DirtyCacheEntry[_hashDirties.size()];
Iterator<DirtyCacheEntry> iter = _hashDirties.valuesIterator();
for(int i = 0;i<vals.length;i++){
vals[i] = iter.next();
}
iter = null;
java.util.Arrays.sort(vals,DIRTY_COMPARATOR);
for(int i = 0;i<vals.length;i++){
final DirtyCacheEntry entry = vals[i];
vals[i] = null;
super.update(entry._recid, entry._obj, entry._serializer);
_hashDirties.remove(entry._recid);
}
//update may have triggered more records to be added into dirties, so repeat until all records are written.
}
}
} catch (IOException e) {
throw new IOError(e);
}
}
}

View File

@ -0,0 +1,350 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import javax.crypto.Cipher;
import java.io.IOException;
/**
* A DB wrapping and caching another DB.
*
* @author Jan Kotek
* @author Alex Boisvert
* @author Cees de Groot
*
* TODO add 'cache miss' statistics
*/
class DBCacheMRU
extends DBCache {
private static final boolean debug = false;
/**
* Cached object hashtable
*/
protected LongHashMap<CacheEntry> _hash;
/**
* Maximum number of objects in the cache.
*/
protected int _max;
/**
* Beginning of linked-list of cache elements. First entry is element
* which has been used least recently.
*/
protected CacheEntry _first;
/**
* End of linked-list of cache elements. Last entry is element
* which has been used most recently.
*/
protected CacheEntry _last;
/**
* Construct a CacheRecordManager wrapping another DB and
* using a given cache policy.
*/
public DBCacheMRU(String filename, boolean readonly, boolean transactionDisabled,
Cipher cipherIn, Cipher cipherOut, boolean useRandomAccessFile,
boolean deleteFilesAfterClose, int cacheMaxRecords, boolean lockingDisabled) {
super(filename, readonly, transactionDisabled,
cipherIn, cipherOut, useRandomAccessFile,
deleteFilesAfterClose,lockingDisabled);
_hash = new LongHashMap<CacheEntry>(cacheMaxRecords);
_max = cacheMaxRecords;
}
public synchronized <A> A fetch(long recid, Serializer<A> serializer, boolean disableCache) throws IOException {
if (disableCache)
return super.fetch(recid, serializer, disableCache);
else
return fetch(recid, serializer);
}
public synchronized void delete(long recid)
throws IOException {
checkNotClosed();
super.delete(recid);
synchronized (_hash){
CacheEntry entry = _hash.get(recid);
if (entry != null) {
removeEntry(entry);
_hash.remove(entry._recid);
}
_hashDirties.remove(recid);
}
if(super.needsAutoCommit())
commit();
}
public synchronized <A> void update(final long recid, final A obj, final Serializer<A> serializer) throws IOException {
checkNotClosed();
synchronized (_hash){
//remove entry if it already exists
CacheEntry entry = cacheGet(recid);
if (entry != null) {
_hash.remove(recid);
removeEntry(entry);
}
//check if entry is in dirties, in this case just update its object
DirtyCacheEntry e = _hashDirties.get(recid);
if(e!=null){
if(recid!=e._recid) throw new Error();
e._obj = obj;
e._serializer = serializer;
return;
}
//create new dirty entry
e = new DirtyCacheEntry();
e._recid = recid;
e._obj = obj;
e._serializer = serializer;
_hashDirties.put(recid,e);
}
if(super.needsAutoCommit())
commit();
}
public synchronized <A> A fetch(long recid, Serializer<A> serializer)
throws IOException {
checkNotClosed();
final CacheEntry entry = cacheGet(recid);
if (entry != null) {
return (A) entry._obj;
}
//check dirties
final DirtyCacheEntry entry2 = _hashDirties.get(recid);
if(entry2!=null){
return (A) entry2._obj;
}
A value = super.fetch(recid, serializer);
if(super.needsAutoCommit())
commit();
//put record into MRU cache
cachePut(recid, value);
return value;
}
public synchronized void close() {
if(isClosed())
return;
updateCacheEntries();
super.close();
_hash = null;
}
public synchronized void rollback() {
// discard all cache entries since we don't know which entries
// where part of the transaction
synchronized (_hash){
_hash.clear();
_first = null;
_last = null;
}
super.rollback();
}
/**
* Obtain an object in the cache
*/
protected CacheEntry cacheGet(long key) {
synchronized (_hash){
CacheEntry entry = _hash.get(key);
if ( entry != null && _last != entry) {
//touch entry
removeEntry(entry);
addEntry(entry);
}
return entry;
}
}
/**
* Place an object in the cache.
*
* @throws IOException
*/
protected void cachePut(final long recid, final Object value) throws IOException {
synchronized (_hash){
CacheEntry entry = _hash.get(recid);
if (entry != null) {
entry._obj = value;
//touch entry
if (_last != entry) {
removeEntry(entry);
addEntry(entry);
}
} else {
if (_hash.size() >= _max) {
// purge and recycle entry
entry = purgeEntry();
entry._recid = recid;
entry._obj = value;
} else {
entry = new CacheEntry(recid, value);
}
addEntry(entry);
_hash.put(entry._recid, entry);
}
}
}
/**
* Add a CacheEntry. Entry goes at the end of the list.
*/
protected void addEntry(CacheEntry entry) {
synchronized (_hash){
if (_first == null) {
_first = entry;
_last = entry;
} else {
_last._next = entry;
entry._previous = _last;
_last = entry;
}
}
}
/**
* Remove a CacheEntry from linked list
*/
protected void removeEntry(CacheEntry entry) {
synchronized (_hash){
if (entry == _first) {
_first = entry._next;
}
if (_last == entry) {
_last = entry._previous;
}
CacheEntry previous = entry._previous;
CacheEntry next = entry._next;
if (previous != null) {
previous._next = next;
}
if (next != null) {
next._previous = previous;
}
entry._previous = null;
entry._next = null;
}
}
/**
* Purge least recently used object from the cache
*
* @return recyclable CacheEntry
*/
protected CacheEntry purgeEntry() {
synchronized (_hash){
CacheEntry entry = _first;
if (entry == null)
return new CacheEntry(-1, null);
removeEntry(entry);
_hash.remove(entry._recid);
entry._obj = null;
return entry;
}
}
@SuppressWarnings("unchecked")
static final class CacheEntry {
protected long _recid;
protected Object _obj;
protected CacheEntry _previous;
protected CacheEntry _next;
CacheEntry(long recid, Object obj) {
_recid = recid;
_obj = obj;
}
}
public void clearCache() {
if(debug)
System.err.println("DBCache: Clear cache");
// discard all cache entries since we don't know which entries
// where part of the transaction
synchronized (_hash){
_hash.clear();
_first = null;
_last = null;
//clear dirties
updateCacheEntries();
}
}
}

View File

@ -0,0 +1,401 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import javax.crypto.Cipher;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A DB wrapping and caching another DB.
*
* @author Jan Kotek
* @author Alex Boisvert
* @author Cees de Groot
*
* TODO add 'cache miss' statistics
*/
public class DBCacheRef
extends DBCache {
private static final boolean debug = false;
/**
* If Soft Cache is enabled, this contains softly referenced clean entries.
* If entry became dirty, it is moved to _hash with limited size.
* This map is accessed from SoftCache Disposer thread, so all access must be
* synchronized
*/
protected LongHashMap _softHash;
/**
* Reference queue used to collect Soft Cache entries
*/
protected ReferenceQueue<ReferenceCacheEntry> _refQueue;
/**
* Thread in which Soft Cache references are disposed
*/
protected Thread _softRefThread;
protected static AtomicInteger threadCounter = new AtomicInteger(0);
/** counter which counts number of insert since last 'action'*/
protected int insertCounter = 0;
private final boolean _autoClearReferenceCacheOnLowMem;
private final byte _cacheType;
/**
* Construct a CacheRecordManager wrapping another DB and
* using a given cache policy.
*/
public DBCacheRef(String filename, boolean readonly, boolean transactionDisabled,
Cipher cipherIn, Cipher cipherOut, boolean useRandomAccessFile,
boolean deleteFilesAfterClose,
byte cacheType, boolean cacheAutoClearOnLowMem, boolean lockingDisabled) {
super(filename, readonly, transactionDisabled,
cipherIn, cipherOut, useRandomAccessFile,
deleteFilesAfterClose, lockingDisabled);
this._cacheType = cacheType;
_autoClearReferenceCacheOnLowMem = cacheAutoClearOnLowMem;
_softHash = new LongHashMap<ReferenceCacheEntry>();
_refQueue = new ReferenceQueue<ReferenceCacheEntry>();
_softRefThread = new Thread(
new SoftRunnable(this, _refQueue),
"JDBM Soft Cache Disposer " + (threadCounter.incrementAndGet()));
_softRefThread.setDaemon(true);
_softRefThread.start();
}
void clearCacheIfLowOnMem() {
insertCounter = 0;
if(!_autoClearReferenceCacheOnLowMem)
return;
Runtime r = Runtime.getRuntime();
long max = r.maxMemory();
if(max == Long.MAX_VALUE)
return;
double free = r.freeMemory();
double total = r.totalMemory();
//We believe that free refers to total not max.
//Increasing heap size to max would increase to max
free = free + (max-total);
if(debug)
System.err.println("DBCache: freemem = " +free + " = "+(free/max)+"%");
if(free<1e7 || free*4 <max)
clearCache();
}
public synchronized <A> A fetch(long recid, Serializer<A> serializer, boolean disableCache) throws IOException {
if (disableCache)
return super.fetch(recid, serializer, disableCache);
else
return fetch(recid, serializer);
}
public synchronized void delete(long recid)
throws IOException {
checkNotClosed();
super.delete(recid);
synchronized (_hashDirties){
_hashDirties.remove(recid);
}
synchronized (_softHash) {
Object e = _softHash.remove(recid);
if (e != null && e instanceof ReferenceCacheEntry) {
((ReferenceCacheEntry)e).clear();
}
}
if(needsAutoCommit())
commit();
}
public synchronized <A> void update(final long recid, A obj, Serializer<A> serializer) throws IOException {
checkNotClosed();
synchronized (_softHash) {
//soft cache can not contain dirty objects
Object e = _softHash.remove(recid);
if (e != null && e instanceof ReferenceCacheEntry) {
((ReferenceCacheEntry)e).clear();
}
}
synchronized (_hashDirties){
//put into dirty cache
final DirtyCacheEntry e = new DirtyCacheEntry();
e._recid = recid;
e._obj = obj;
e._serializer = serializer;
_hashDirties.put(recid,e);
}
if(needsAutoCommit())
commit();
}
public synchronized <A> A fetch(long recid, Serializer<A> serializer)
throws IOException {
checkNotClosed();
synchronized (_softHash) {
Object e = _softHash.get(recid);
if (e != null) {
if(e instanceof ReferenceCacheEntry)
e = ((ReferenceCacheEntry)e).get();
if (e != null) {
return (A) e;
}
}
}
synchronized (_hashDirties){
DirtyCacheEntry e2 = _hashDirties.get(recid);
if(e2!=null){
return (A) e2._obj;
}
}
A value = super.fetch(recid, serializer);
if(needsAutoCommit())
commit();
synchronized (_softHash) {
if (_cacheType == SOFT)
_softHash.put(recid, new SoftCacheEntry(recid, value, _refQueue));
else if (_cacheType == WEAK)
_softHash.put(recid, new WeakCacheEntry(recid, value, _refQueue));
else
_softHash.put(recid,value);
}
return value;
}
public synchronized void close() {
checkNotClosed();
updateCacheEntries();
super.close();
_softHash = null;
_softRefThread.interrupt();
}
public synchronized void rollback() {
checkNotClosed();
// discard all cache entries since we don't know which entries
// where part of the transaction
synchronized (_softHash) {
Iterator<ReferenceCacheEntry> iter = _softHash.valuesIterator();
while (iter.hasNext()) {
ReferenceCacheEntry e = iter.next();
e.clear();
}
_softHash.clear();
}
super.rollback();
}
protected boolean isCacheEntryDirty(DirtyCacheEntry entry) {
return _hashDirties.get(entry._recid) != null;
}
protected void setCacheEntryDirty(DirtyCacheEntry entry, boolean dirty) {
if (dirty) {
_hashDirties.put(entry._recid, entry);
} else {
_hashDirties.remove(entry._recid);
}
}
interface ReferenceCacheEntry {
long getRecid();
void clear();
Object get();
}
@SuppressWarnings("unchecked")
static final class SoftCacheEntry extends SoftReference implements ReferenceCacheEntry {
protected final long _recid;
public long getRecid() {
return _recid;
}
SoftCacheEntry(long recid, Object obj, ReferenceQueue queue) {
super(obj, queue);
_recid = recid;
}
}
@SuppressWarnings("unchecked")
static final class WeakCacheEntry extends WeakReference implements ReferenceCacheEntry {
protected final long _recid;
public long getRecid() {
return _recid;
}
WeakCacheEntry(long recid, Object obj, ReferenceQueue queue) {
super(obj, queue);
_recid = recid;
}
}
/**
* Runs in separate thread and cleans SoftCache.
* Runnable auto exists when CacheRecordManager is GCed
*
* @author Jan Kotek
*/
static final class SoftRunnable implements Runnable {
private ReferenceQueue<ReferenceCacheEntry> entryQueue;
private WeakReference<DBCacheRef> db2;
public SoftRunnable(DBCacheRef db,
ReferenceQueue<ReferenceCacheEntry> entryQueue) {
this.db2 = new WeakReference<DBCacheRef>(db);
this.entryQueue = entryQueue;
}
public void run() {
while (true) try {
//collect next item from cache,
//limit 10000 ms is to keep periodically checking if db was GCed
ReferenceCacheEntry e = (ReferenceCacheEntry) entryQueue.remove(10000);
//check if db was GCed, cancel in that case
DBCacheRef db = db2.get();
if (db == null)
return;
if (e != null) {
synchronized (db._softHash) {
int counter = 0;
while (e != null) {
db._softHash.remove(e.getRecid());
e = (SoftCacheEntry) entryQueue.poll();
if(debug)
counter++;
}
if(debug)
System.err.println("DBCache: "+counter+" objects released from ref cache.");
}
}else{
//check memory consumption every 10 seconds
db.clearCacheIfLowOnMem();
}
} catch (InterruptedException e) {
return;
} catch (Throwable e) {
//this thread must keep spinning,
//otherwise SoftCacheEntries would not be disposed
e.printStackTrace();
}
}
}
public void clearCache() {
if(debug)
System.err.println("DBCache: Clear cache");
synchronized (_softHash) {
if(_cacheType!=HARD){
Iterator<ReferenceCacheEntry> iter = _softHash.valuesIterator();
while (iter.hasNext()) {
ReferenceCacheEntry e = iter.next();
e.clear();
}
}
_softHash.clear();
}
}
}

View File

@ -0,0 +1,351 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOError;
import java.security.spec.KeySpec;
/**
* Class used to configure and create DB. It uses builder pattern.
*/
public class DBMaker {
private byte cacheType = DBCacheRef.MRU;
private int mruCacheSize = 2048;
private String location = null;
private boolean disableTransactions = false;
private boolean lockingDisabled = false;
private boolean readonly = false;
private String password = null;
private boolean useAES256Bit = true;
private boolean useRandomAccessFile = false;
private boolean autoClearRefCacheOnLowMem = true;
private boolean closeOnJVMExit = false;
private boolean deleteFilesAfterCloseFlag = false;
private DBMaker(){}
/**
* Creates new DBMaker and sets file to load data from.
* @param file to load data from
* @return new DBMaker
*/
public static DBMaker openFile(String file){
DBMaker m = new DBMaker();
m.location = file;
return m;
}
/**
* Creates new DBMaker which uses in memory store. Data will be lost after JVM exits.
* @return new DBMaker
*/
public static DBMaker openMemory(){
return new DBMaker();
}
/**
* Open store in zip file
*
* @param zip file
* @return new DBMaker
*/
public static DBMaker openZip(String zip) {
DBMaker m = new DBMaker();
m.location = "$$ZIP$$://"+zip;
return m;
}
static String isZipFileLocation(String location){
String match = "$$ZIP$$://";
if( location.startsWith(match)){
return location.substring(match.length());
}
return null;
}
/**
* Use WeakReference for cache.
* This cache does not improve performance much,
* but prevents JDBM from creating multiple instances of the same object.
*
* @return this builder
*/
public DBMaker enableWeakCache() {
cacheType = DBCacheRef.WEAK;
return this;
}
/**
* Use SoftReference for cache.
* This cache greatly improves performance if you have enoguth memory.
* Instances in cache are Garbage Collected when memory gets low
*
* @return this builder
*/
public DBMaker enableSoftCache() {
cacheType = DBCacheRef.SOFT;
return this;
}
/**
* Use hard reference for cache.
* This greatly improves performance if there is enought memory
* Hard cache has smaller memory overhead then Soft or Weak, because
* reference objects and queue does not have to be maintained
*
* @return this builder
*/
public DBMaker enableHardCache() {
cacheType = DBCacheRef.SOFT;
return this;
}
/**
* Use 'Most Recently Used' cache with limited size.
* Oldest instances are released from cache when new instances are fetched.
* This cache is not cleared by GC. Is good for systems with limited memory.
* <p/>
* Default size for MRU cache is 2048 records.
*
* @return this builder
*/
public DBMaker enableMRUCache() {
cacheType = DBCacheRef.MRU;
return this;
}
/**
*
* Sets 'Most Recently Used' cache size. This cache is activated by default with size 2048
*
* @param cacheSize number of instances which will be kept in cache.
* @return this builder
*/
public DBMaker setMRUCacheSize(int cacheSize) {
if (cacheSize < 0) throw new IllegalArgumentException("Cache size is smaller than zero");
cacheType = DBCacheRef.MRU;
mruCacheSize = cacheSize;
return this;
}
/**
* If reference (soft,weak or hard) cache is enabled,
* GC may not release references fast enough (or not at all in case of hard cache).
* So JDBM periodically checks amount of free heap memory.
* If free memory is less than 25% or 10MB,
* JDBM completely clears its reference cache to prevent possible memory issues.
* <p>
* Calling this method disables auto cache clearing when mem is low.
* And of course it can cause some out of memory exceptions.
*
* @return this builder
*/
public DBMaker disableCacheAutoClear(){
this.autoClearRefCacheOnLowMem = false;
return this;
}
/**
* Enabled storage encryption using AES cipher. JDBM supports both 128 bit and 256 bit encryption if JRE provides it.
* There are some restrictions on AES 256 bit and not all JREs have it by default.
* <p/>
* Storage can not be read (decrypted), unless the key is provided next time it is opened
*
* @param password used to encrypt store
* @param useAES256Bit if true strong AES 256 bit encryption is used. Otherwise more usual AES 128 bit is used.
* @return this builder
*/
public DBMaker enableEncryption(String password, boolean useAES256Bit) {
this.password = password;
this.useAES256Bit = useAES256Bit;
return this;
}
/**
* Make DB readonly.
* Update/delete/insert operation will throw 'UnsupportedOperationException'
*
* @return this builder
*/
public DBMaker readonly() {
readonly = true;
return this;
}
/**
* Disable cache completely
*
* @return this builder
*/
public DBMaker disableCache() {
cacheType = DBCacheRef.NONE;
return this;
}
/**
* Option to disable transaction (to increase performance at the cost of potential data loss).
* Transactions are enabled by default
* <p/>
* Switches off transactioning for the record manager. This means
* that a) a transaction log is not kept, and b) writes aren't
* synch'ed after every update. Writes are cached in memory and then flushed
* to disk every N writes. You may also flush writes manually by calling commit().
* This is useful when batch inserting into a new database.
* <p/>
* When using this, database must be properly closed before JVM shutdown.
* Failing to do so may and WILL corrupt store.
*
* @return this builder
*/
public DBMaker disableTransactions() {
this.disableTransactions = true;
return this;
}
/**
* Disable file system based locking (for file systems that do not support it).
*
* Locking is not supported by many remote or distributed file systems; such
* as Lustre and NFS. Attempts to perform locks will result in an
* IOException with the message "Function not implemented".
*
* Disabling locking will avoid this issue, though of course it comes with
* all the issues of uncontrolled file access.
*
* @return this builder
*/
public DBMaker disableLocking(){
this.lockingDisabled = true;
return this;
}
/**
* By default JDBM uses mapped memory buffers to read from files.
* But this may behave strangely on some platforms.
* Safe alternative is to use old RandomAccessFile rather then mapped ByteBuffer.
* There is typically slower (pages needs to be copyed into memory on every write).
*
* @return this builder
*/
public DBMaker useRandomAccessFile(){
this.useRandomAccessFile = true;
return this;
}
/**
* Registers shutdown hook and close database on JVM exit, if it was not already closed;
*
* @return this builder
*/
public DBMaker closeOnExit(){
this.closeOnJVMExit = true;
return this;
}
/**
* Delete all storage files after DB is closed
*
* @return this builder
*/
public DBMaker deleteFilesAfterClose(){
this.deleteFilesAfterCloseFlag = true;
return this;
}
/**
* Opens database with settings earlier specified in this builder.
*
* @return new DB
* @throws java.io.IOError if db could not be opened
*/
public DB make() {
Cipher cipherIn = null;
Cipher cipherOut = null;
if (password != null) try {
//initialize ciphers
//this code comes from stack owerflow
//http://stackoverflow.com/questions/992019/java-256bit-aes-encryption/992413#992413
byte[] salt = new byte[]{3, -34, 123, 53, 78, 121, -12, -1, 45, -12, -48, 89, 11, 100, 99, 8};
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 1024, useAES256Bit?256:128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
String transform = "AES/CBC/NoPadding";
IvParameterSpec params = new IvParameterSpec(salt);
cipherIn = Cipher.getInstance(transform);
cipherIn.init(Cipher.ENCRYPT_MODE, secret, params);
cipherOut = Cipher.getInstance(transform);
cipherOut.init(Cipher.DECRYPT_MODE, secret, params);
//sanity check, try with page size
byte[] data = new byte[Storage.PAGE_SIZE];
byte[] encData = cipherIn.doFinal(data);
if (encData.length != Storage.PAGE_SIZE)
throw new Error("Page size changed after encryption, make sure you use '/NoPadding'");
byte[] data2 = cipherOut.doFinal(encData);
for (int i = 0; i < data.length; i++) {
if (data[i] != data2[i]) throw new Error("Encryption provided by JRE does not work");
}
} catch (Exception e) {
throw new IOError(e);
}
DBAbstract db = null;
if (cacheType == DBCacheRef.MRU){
db = new DBCacheMRU(location, readonly, disableTransactions, cipherIn, cipherOut,useRandomAccessFile,deleteFilesAfterCloseFlag, mruCacheSize,lockingDisabled);
}else if( cacheType == DBCacheRef.SOFT || cacheType == DBCacheRef.HARD || cacheType == DBCacheRef.WEAK) {
db = new DBCacheRef(location, readonly, disableTransactions, cipherIn, cipherOut,useRandomAccessFile,deleteFilesAfterCloseFlag, cacheType,autoClearRefCacheOnLowMem,lockingDisabled);
} else if (cacheType == DBCacheRef.NONE) {
db = new DBStore(location, readonly, disableTransactions, cipherIn, cipherOut,useRandomAccessFile,deleteFilesAfterCloseFlag,lockingDisabled);
} else {
throw new IllegalArgumentException("Unknown cache type: " + cacheType);
}
if(closeOnJVMExit){
db.addShutdownHook();
}
return db;
}
}

View File

@ -0,0 +1,928 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import javax.crypto.Cipher;
import java.io.*;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* This class manages records, which are uninterpreted blobs of data. The
* set of operations is simple and straightforward: you communicate with
* the class using long "rowids" and byte[] data blocks. Rowids are returned
* on inserts and you can stash them away someplace safe to be able to get
* back to them. Data blocks can be as long as you wish, and may have
* lengths different from the original when updating.
* <p/>
* Operations are synchronized, so that only one of them will happen
* concurrently even if you hammer away from multiple threads. Operations
* are made atomic by keeping a transaction log which is recovered after
* a crash, so the operations specified by this interface all have ACID
* properties.
* <p/>
* You identify a file by just the name. The package attaches <tt>.db</tt>
* for the database file, and <tt>.lg</tt> for the transaction log. The
* transaction log is synchronized regularly and then restarted, so don't
* worry if you see the size going up and down.
*
* @author Alex Boisvert
* @author Cees de Groot
*/
class DBStore
extends DBAbstract {
/**
* Version of storage. It should be safe to open lower versions, but engine should throw exception
* while opening new versions (as it contains unsupported features or serialization)
*/
static final long STORE_FORMAT_VERSION = 1L;
/**
* Underlying file for store records.
*/
private PageFile _file;
/**
* Page manager for physical manager.
*/
private PageManager _pageman;
/**
* Physical row identifier manager.
*/
private PhysicalRowIdManager _physMgr;
/**
* Indicated that store is opened for readonly operations
* If true, store will throw UnsupportedOperationException when update/insert/delete operation is called
*/
private final boolean readonly;
final boolean transactionsDisabled;
private final boolean deleteFilesAfterClose;
private static final int AUTOCOMMIT_AFTER_N_PAGES = 1024 * 5;
boolean commitInProgress = false;
/**
* cipher used for decryption, may be null
*/
private Cipher cipherOut;
/**
* cipher used for encryption, may be null
*/
private Cipher cipherIn;
private boolean useRandomAccessFile;
private boolean lockingDisabled;
void checkCanWrite() {
if (readonly)
throw new UnsupportedOperationException("Could not write, store is opened as read-only");
}
/**
* Logigal to Physical row identifier manager.
*/
private LogicalRowIdManager _logicMgr;
/**
* Static debugging flag
*/
public static final boolean DEBUG = false;
static final long PREALOCATE_PHYS_RECID = Short.MIN_VALUE;
static final Object PREALOCATE_OBJ = new Object();
private final DataInputOutput buffer = new DataInputOutput();
private boolean bufferInUse = false;
private final String _filename;
public DBStore(String filename, boolean readonly, boolean transactionDisabled, boolean lockingDisabled) throws IOException {
this(filename, readonly, transactionDisabled, null, null, false,false,false);
}
/**
* Creates a record manager for the indicated file
*
* @throws IOException when the file cannot be opened or is not
* a valid file content-wise.
*/
public DBStore(String filename, boolean readonly, boolean transactionDisabled,
Cipher cipherIn, Cipher cipherOut, boolean useRandomAccessFile,
boolean deleteFilesAfterClose, boolean lockingDisabled){
_filename = filename;
this.readonly = readonly;
this.transactionsDisabled = transactionDisabled;
this.cipherIn = cipherIn;
this.cipherOut = cipherOut;
this.useRandomAccessFile = useRandomAccessFile;
this.deleteFilesAfterClose = deleteFilesAfterClose;
this.lockingDisabled = lockingDisabled;
reopen();
}
private void reopen() {
try{
_file = new PageFile(_filename, readonly, transactionsDisabled, cipherIn, cipherOut,useRandomAccessFile,lockingDisabled);
_pageman = new PageManager(_file);
_physMgr = new PhysicalRowIdManager(_file, _pageman);
_logicMgr = new LogicalRowIdManager(_file, _pageman);
long versionNumber = getRoot(STORE_VERSION_NUMBER_ROOT);
if (versionNumber > STORE_FORMAT_VERSION)
throw new IOException("Unsupported version of store. Please update JDBM. Minimal supported ver:" + STORE_FORMAT_VERSION + ", store ver:" + versionNumber);
if (!readonly)
setRoot(STORE_VERSION_NUMBER_ROOT, STORE_FORMAT_VERSION);
}catch(IOException e){
throw new IOError(e);
}
}
/**
* Closes the record manager.
*
* @throws IOException when one of the underlying I/O operations fails.
*/
public synchronized void close() {
checkNotClosed();
try {
super.close();
_pageman.close();
_file.close();
if(deleteFilesAfterClose)
_file.storage.deleteAllFiles();
_pageman = null;
_file = null;
} catch (IOException e) {
throw new IOError(e);
}
}
public boolean isClosed() {
return _pageman==null;
}
public synchronized <A> long insert(final A obj, final Serializer<A> serializer, final boolean disableCache)
throws IOException {
checkNotClosed();
checkCanWrite();
if (needsAutoCommit()) {
commit();
}
if (bufferInUse) {
//current reusable buffer is in use, have to fallback into creating new instances
DataInputOutput buffer2 = new DataInputOutput();
return insert2(obj, serializer, buffer2);
}
try {
bufferInUse = true;
return insert2(obj, serializer, buffer);
} finally {
bufferInUse = false;
}
}
boolean needsAutoCommit() {
return transactionsDisabled && !commitInProgress &&
(_file.getDirtyPageCount() >= AUTOCOMMIT_AFTER_N_PAGES );
}
private <A> long insert2(A obj, Serializer<A> serializer, DataInputOutput buf)
throws IOException {
buf.reset();
long physRowId;
if(obj==PREALOCATE_OBJ){
//if inserted record is PREALOCATE_OBJ , it gets special handling.
//it is inserted only into _logicMgr with special value to indicate null
//this is used to preallocate recid for lazy inserts in cache
physRowId = PREALOCATE_PHYS_RECID;
}else{
serializer.serialize(buf, obj);
if(buf.getPos()>RecordHeader.MAX_RECORD_SIZE){
throw new IllegalArgumentException("Too big record. JDBM only supports record size up to: "+RecordHeader.MAX_RECORD_SIZE+" bytes. Record size was: "+buf.getPos());
}
physRowId = _physMgr.insert(buf.getBuf(), 0, buf.getPos());
}
final long recid = _logicMgr.insert(physRowId);
if (DEBUG) {
System.out.println("BaseRecordManager.insert() recid " + recid + " length " + buf.getPos());
}
return compressRecid(recid);
}
public synchronized void delete(long logRowId)
throws IOException {
checkNotClosed();
checkCanWrite();
if (logRowId <= 0) {
throw new IllegalArgumentException("Argument 'recid' is invalid: "
+ logRowId);
}
if (needsAutoCommit()) {
commit();
}
if (DEBUG) {
System.out.println("BaseRecordManager.delete() recid " + logRowId);
}
logRowId = decompressRecid(logRowId);
long physRowId = _logicMgr.fetch(logRowId);
_logicMgr.delete(logRowId);
if(physRowId!=PREALOCATE_PHYS_RECID){
_physMgr.free(physRowId);
}
}
public synchronized <A> void update(long recid, A obj, Serializer<A> serializer)
throws IOException {
checkNotClosed();
checkCanWrite();
if (recid <= 0) {
throw new IllegalArgumentException("Argument 'recid' is invalid: "
+ recid);
}
if (needsAutoCommit()) {
commit();
}
if (bufferInUse) {
//current reusable buffer is in use, have to create new instances
DataInputOutput buffer2 = new DataInputOutput();
update2(recid, obj, serializer, buffer2);
return;
}
try {
bufferInUse = true;
update2(recid, obj, serializer, buffer);
} finally {
bufferInUse = false;
}
}
private <A> void update2(long logRecid, final A obj, final Serializer<A> serializer, final DataInputOutput buf)
throws IOException {
logRecid = decompressRecid(logRecid);
long physRecid = _logicMgr.fetch(logRecid);
if (physRecid == 0)
throw new IOException("Can not update, recid does not exist: " + logRecid);
buf.reset();
serializer.serialize(buf, obj);
if (DEBUG) {
System.out.println("BaseRecordManager.update() recid " + logRecid + " length " + buf.getPos());
}
long newRecid =
physRecid!=PREALOCATE_PHYS_RECID?
_physMgr.update(physRecid, buf.getBuf(), 0, buf.getPos()):
//previous record was only virtual and does not actually exist, so make new insert
_physMgr.insert(buf.getBuf(),0,buf.getPos());
_logicMgr.update(logRecid, newRecid);
}
public synchronized <A> A fetch(final long recid, final Serializer<A> serializer)
throws IOException {
checkNotClosed();
if (recid <= 0) {
throw new IllegalArgumentException("Argument 'recid' is invalid: " + recid);
}
if (bufferInUse) {
//current reusable buffer is in use, have to create new instances
DataInputOutput buffer2 = new DataInputOutput();
return fetch2(recid, serializer, buffer2);
}
try {
bufferInUse = true;
return fetch2(recid, serializer, buffer);
} finally {
bufferInUse = false;
}
}
public synchronized <A> A fetch(long recid, Serializer<A> serializer, boolean disableCache) throws IOException {
//we dont have any cache, so can ignore disableCache parameter
return fetch(recid, serializer);
}
private <A> A fetch2(long recid, final Serializer<A> serializer, final DataInputOutput buf)
throws IOException {
recid = decompressRecid(recid);
buf.reset();
long physLocation = _logicMgr.fetch(recid);
if (physLocation == 0) {
//throw new IOException("Record not found, recid: "+recid);
return null;
}
if(physLocation == PREALOCATE_PHYS_RECID){
throw new InternalError("cache should prevent this!");
}
_physMgr.fetch(buf, physLocation);
if (DEBUG) {
System.out.println("BaseRecordManager.fetch() recid " + recid + " length " + buf.getPos());
}
buf.resetForReading();
try {
return serializer.deserialize(buf); //TODO there should be write limit to throw EOFException
} catch (ClassNotFoundException e) {
throw new IOError(e);
}
}
byte[] fetchRaw(long recid) throws IOException {
recid = decompressRecid(recid);
long physLocation = _logicMgr.fetch(recid);
if (physLocation == 0) {
//throw new IOException("Record not found, recid: "+recid);
return null;
}
DataInputOutput i = new DataInputOutput();
_physMgr.fetch(i, physLocation);
return i.toByteArray();
}
public synchronized long getRoot(final byte id){
checkNotClosed();
return _pageman.getFileHeader().fileHeaderGetRoot(id);
}
public synchronized void setRoot(final byte id, final long rowid){
checkNotClosed();
checkCanWrite();
_pageman.getFileHeader().fileHeaderSetRoot(id, rowid);
}
public synchronized void commit() {
try {
commitInProgress = true;
checkNotClosed();
checkCanWrite();
/** flush free phys rows into pages*/
_physMgr.commit();
_logicMgr.commit();
/**commit pages */
_pageman.commit();
} catch (IOException e) {
throw new IOError(e);
}finally {
commitInProgress= false;
}
}
public synchronized void rollback() {
if (transactionsDisabled)
throw new IllegalAccessError("Transactions are disabled, can not rollback");
try {
checkNotClosed();
_physMgr.rollback();
_logicMgr.rollback();
_pageman.rollback();
super.rollback();
} catch (IOException e) {
throw new IOError(e);
}
}
public void copyToZip(String zipFile) {
try {
String zip = zipFile;
String zip2 = "db";
ZipOutputStream z = new ZipOutputStream(new FileOutputStream(zip));
//copy zero pages
{
String file = zip2 + 0;
z.putNextEntry(new ZipEntry(file));
z.write(Utils.encrypt(cipherIn, _pageman.getHeaderBufData()));
z.closeEntry();
}
//iterate over pages and create new file for each
for (long pageid = _pageman.getFirst(Magic.TRANSLATION_PAGE);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
PageIo page = _file.get(pageid);
String file = zip2 + pageid;
z.putNextEntry(new ZipEntry(file));
z.write(Utils.encrypt(cipherIn, page.getData()));
z.closeEntry();
_file.release(page);
}
for (long pageid = _pageman.getFirst(Magic.FREELOGIDS_PAGE);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
PageIo page = _file.get(pageid);
String file = zip2 + pageid;
z.putNextEntry(new ZipEntry(file));
z.write(Utils.encrypt(cipherIn, page.getData()));
z.closeEntry();
_file.release(page);
}
for (long pageid = _pageman.getFirst(Magic.USED_PAGE);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
PageIo page = _file.get(pageid);
String file = zip2 + pageid;
z.putNextEntry(new ZipEntry(file));
z.write(Utils.encrypt(cipherIn, page.getData()));
z.closeEntry();
_file.release(page);
}
for (long pageid = _pageman.getFirst(Magic.FREEPHYSIDS_PAGE);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
PageIo page = _file.get(pageid);
String file = zip2 + pageid;
z.putNextEntry(new ZipEntry(file));
z.write(Utils.encrypt(cipherIn, page.getData()));
z.closeEntry();
_file.release(page);
}
for (long pageid = _pageman.getFirst(Magic.FREEPHYSIDS_ROOT_PAGE);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
PageIo page = _file.get(pageid);
String file = zip2 + pageid;
z.putNextEntry(new ZipEntry(file));
z.write(Utils.encrypt(cipherIn, page.getData()));
z.closeEntry();
_file.release(page);
}
z.close();
} catch (IOException e) {
throw new IOError(e);
}
}
public synchronized void clearCache() {
//no cache
}
private long statisticsCountPages(short pageType) throws IOException {
long pageCounter = 0;
for (long pageid = _pageman.getFirst(pageType);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
pageCounter++;
}
return pageCounter;
}
public synchronized String calculateStatistics() {
checkNotClosed();
try {
final StringBuilder b = new StringBuilder();
//count pages
{
b.append("PAGES:\n");
long total = 0;
long pages = statisticsCountPages(Magic.USED_PAGE);
total += pages;
b.append(" " + pages + " used pages with size " + Utils.formatSpaceUsage(pages * Storage.PAGE_SIZE) + "\n");
pages = statisticsCountPages(Magic.TRANSLATION_PAGE);
total += pages;
b.append(" " + pages + " record translation pages with size " + Utils.formatSpaceUsage(pages * Storage.PAGE_SIZE) + "\n");
pages = statisticsCountPages(Magic.FREE_PAGE);
total += pages;
b.append(" " + pages + " free (unused) pages with size " + Utils.formatSpaceUsage(pages * Storage.PAGE_SIZE) + "\n");
pages = statisticsCountPages(Magic.FREEPHYSIDS_PAGE);
total += pages;
b.append(" " + pages + " free (phys) pages with size " + Utils.formatSpaceUsage(pages * Storage.PAGE_SIZE) + "\n");
pages = statisticsCountPages(Magic.FREELOGIDS_PAGE);
total += pages;
b.append(" " + pages + " free (logical) pages with size " + Utils.formatSpaceUsage(pages * Storage.PAGE_SIZE) + "\n");
b.append(" Total number of pages is " + total + " with size " + Utils.formatSpaceUsage(total * Storage.PAGE_SIZE) + "\n");
}
{
b.append("RECORDS:\n");
long recordCount = 0;
long freeRecordCount = 0;
long maximalRecordSize = 0;
long maximalAvailSizeDiff = 0;
long totalRecordSize = 0;
long totalAvailDiff = 0;
//count records
for (long pageid = _pageman.getFirst(Magic.TRANSLATION_PAGE);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
PageIo io = _file.get(pageid);
for (int i = 0; i < _logicMgr.ELEMS_PER_PAGE; i += 1) {
final int pos = Magic.PAGE_HEADER_SIZE + i * Magic.PhysicalRowId_SIZE;
final long physLoc = io.pageHeaderGetLocation((short) pos);
if (physLoc == 0) {
freeRecordCount++;
continue;
}
if(physLoc == PREALOCATE_PHYS_RECID){
continue;
}
recordCount++;
//get size
PageIo page = _file.get(physLoc>>> Storage.PAGE_SIZE_SHIFT);
final short physOffset =(short) (physLoc & Storage.OFFSET_MASK);
int availSize = RecordHeader.getAvailableSize(page, physOffset);
int currentSize = RecordHeader.getCurrentSize(page, physOffset);
_file.release(page);
maximalAvailSizeDiff = Math.max(maximalAvailSizeDiff, availSize - currentSize);
maximalRecordSize = Math.max(maximalRecordSize, currentSize);
totalAvailDiff += availSize - currentSize;
totalRecordSize += currentSize;
}
_file.release(io);
}
b.append(" Contains " + recordCount + " records and " + freeRecordCount + " free slots.\n");
b.append(" Total space occupied by data is " + Utils.formatSpaceUsage(totalRecordSize) + "\n");
b.append(" Average data size in record is " + Utils.formatSpaceUsage(Math.round(1D * totalRecordSize / recordCount)) + "\n");
b.append(" Maximal data size in record is " + Utils.formatSpaceUsage(maximalRecordSize) + "\n");
b.append(" Space wasted in record fragmentation is " + Utils.formatSpaceUsage(totalAvailDiff) + "\n");
b.append(" Maximal space wasted in single record fragmentation is " + Utils.formatSpaceUsage(maximalAvailSizeDiff) + "\n");
}
return b.toString();
} catch (IOException e) {
throw new IOError(e);
}
}
public synchronized void defrag(boolean sortCollections) {
try {
checkNotClosed();
checkCanWrite();
commit();
final String filename2 = _filename + "_defrag" + System.currentTimeMillis();
final String filename1 = _filename;
DBStore db2 = new DBStore(filename2, false, true, cipherIn, cipherOut, false,false,false);
//recreate logical file with original page layout
{
//find minimal logical pageid (logical pageids are negative)
LongHashMap<String> logicalPages = new LongHashMap<String>();
long minpageid = 0;
for (long pageid = _pageman.getFirst(Magic.TRANSLATION_PAGE);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
minpageid = Math.min(minpageid, pageid);
logicalPages.put(pageid, Utils.EMPTY_STRING);
}
//fill second db with logical pages
long pageCounter = 0;
for (
long pageid = db2._pageman.allocate(Magic.TRANSLATION_PAGE);
pageid >= minpageid;
pageid = db2._pageman.allocate(Magic.TRANSLATION_PAGE)
) {
pageCounter++;
if (pageCounter % 1000 == 0)
db2.commit();
}
logicalPages = null;
}
//reinsert collections so physical records are located near each other
//iterate over named object recids, it is sorted with TreeSet
if(sortCollections){
long nameRecid = getRoot(NAME_DIRECTORY_ROOT);
Collection<Long> recids = new TreeSet<Long>();
if(nameRecid!=0){
HTree<String,Long> m = fetch(nameRecid);
recids.addAll(m.values());
}
for (Long namedRecid : recids) {
Object obj = fetch(namedRecid);
if (obj instanceof LinkedList) {
LinkedList2.defrag(namedRecid, this, db2);
} else if (obj instanceof HTree) {
HTree.defrag(namedRecid, this, db2);
} else if (obj instanceof BTree) {
BTree.defrag(namedRecid, this, db2);
}
}
}
for (long pageid = _pageman.getFirst(Magic.TRANSLATION_PAGE);
pageid != 0;
pageid = _pageman.getNext(pageid)
) {
PageIo io = _file.get(pageid);
for (int i = 0; i < _logicMgr.ELEMS_PER_PAGE; i += 1) {
final int pos = Magic.PAGE_HEADER_SIZE + i * Magic.PhysicalRowId_SIZE;
if (pos > Short.MAX_VALUE)
throw new Error();
//write to new file
final long logicalRowId = ((-pageid) << Storage.PAGE_SIZE_SHIFT) + (long) pos;
//read from logical location in second db,
//check if record was already inserted as part of collections
if (db2._pageman.getLast(Magic.TRANSLATION_PAGE) <= pageid &&
db2._logicMgr.fetch(logicalRowId) != 0) {
//yes, this record already exists in second db
continue;
}
//get physical location in this db
final long physRowId = io.pageHeaderGetLocation((short) pos);
if (physRowId == 0)
continue;
if (physRowId == PREALOCATE_PHYS_RECID){
db2._logicMgr.forceInsert(logicalRowId, physRowId);
continue;
}
//read from physical location at this db
DataInputOutput b = new DataInputOutput();
_physMgr.fetch(b, physRowId);
byte[] bb = b.toByteArray();
//force insert into other file, without decompressing logical id to external form
long physLoc = db2._physMgr.insert(bb, 0, bb.length);
db2._logicMgr.forceInsert(logicalRowId, physLoc);
}
_file.release(io);
db2.commit();
}
for(byte b = 0;b<Magic.FILE_HEADER_NROOTS;b++){
db2.setRoot(b, getRoot(b));
}
db2.close();
_pageman.close();
_file.close();
List<File> filesToDelete = new ArrayList<File>();
//now rename old files
String[] exts = {StorageDiskMapped.IDR, StorageDiskMapped.DBR};
for (String ext : exts) {
String f1 = filename1 + ext;
String f2 = filename2 + "_OLD" + ext;
//first rename transaction log
File f1t = new File(f1 + StorageDisk.transaction_log_file_extension);
File f2t = new File(f2 + StorageDisk.transaction_log_file_extension);
f1t.renameTo(f2t);
filesToDelete.add(f2t);
//rename data files, iterate until file exist
for (int i = 0; ; i++) {
File f1d = new File(f1 + "." + i);
if (!f1d.exists()) break;
File f2d = new File(f2 + "." + i);
f1d.renameTo(f2d);
filesToDelete.add(f2d);
}
}
//rename new files
for (String ext : exts) {
String f1 = filename2 + ext;
String f2 = filename1 + ext;
//first rename transaction log
File f1t = new File(f1 + StorageDisk.transaction_log_file_extension);
File f2t = new File(f2 + StorageDisk.transaction_log_file_extension);
f1t.renameTo(f2t);
//rename data files, iterate until file exist
for (int i = 0; ; i++) {
File f1d = new File(f1 + "." + i);
if (!f1d.exists()) break;
File f2d = new File(f2 + "." + i);
f1d.renameTo(f2d);
}
}
for (File d : filesToDelete) {
d.delete();
}
reopen();
} catch (IOException e) {
throw new IOError(e);
}
}
/**
* Insert data at forced logicalRowId, use only for defragmentation !!
*
* @param logicalRowId
* @param data
* @throws IOException
*/
void forceInsert(long logicalRowId, byte[] data) throws IOException {
logicalRowId = decompressRecid(logicalRowId);
if (needsAutoCommit()) {
commit();
}
long physLoc = _physMgr.insert(data, 0, data.length);
_logicMgr.forceInsert(logicalRowId, physLoc);
}
/**
* Returns number of records stored in database.
* Is used for unit tests
*/
long countRecords() throws IOException {
long counter = 0;
long page = _pageman.getFirst(Magic.TRANSLATION_PAGE);
while (page != 0) {
PageIo io = _file.get(page);
for (int i = 0; i < _logicMgr.ELEMS_PER_PAGE; i += 1) {
int pos = Magic.PAGE_HEADER_SIZE + i * Magic.PhysicalRowId_SIZE;
if (pos > Short.MAX_VALUE)
throw new Error();
//get physical location
long physRowId = io.pageHeaderGetLocation((short) pos);
if (physRowId != 0)
counter += 1;
}
_file.release(io);
page = _pageman.getNext(page);
}
return counter;
}
private static int COMPRESS_RECID_PAGE_SHIFT = Integer.MIN_VALUE;
static{
int shift = 1;
while((1<<shift) <LogicalRowIdManager.ELEMS_PER_PAGE )
shift++;
COMPRESS_RECID_PAGE_SHIFT = shift;
}
private final static long COMPRESS_RECID_OFFSET_MASK = 0xFFFFFFFFFFFFFFFFL >>> (64- COMPRESS_RECID_PAGE_SHIFT);
/**
* Compress recid from physical form (block - offset) to (block - slot).
* This way resulting number is smaller and can be easier packed with LongPacker
*/
static long compressRecid(final long recid) {
final long page = recid>>> Storage.PAGE_SIZE_SHIFT;
short offset = (short) (recid & Storage.OFFSET_MASK);
offset = (short) (offset - Magic.PAGE_HEADER_SIZE);
if (offset % Magic.PhysicalRowId_SIZE != 0)
throw new InternalError("recid not dividable "+Magic.PhysicalRowId_SIZE);
long slot = offset / Magic.PhysicalRowId_SIZE;
return (page << COMPRESS_RECID_PAGE_SHIFT) + slot;
}
static long decompressRecid(final long recid) {
final long page = recid >>> COMPRESS_RECID_PAGE_SHIFT;
final short offset = (short) ((recid & COMPRESS_RECID_OFFSET_MASK) * Magic.PhysicalRowId_SIZE + Magic.PAGE_HEADER_SIZE);
return (page << Storage.PAGE_SIZE_SHIFT) + (long) offset;
}
}

View File

@ -0,0 +1,297 @@
package org.apache.jdbm;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Utility class which implements DataInput and DataOutput on top of byte[] buffer
* with minimal overhead
*
* @author Jan Kotek
*/
class DataInputOutput implements DataInput, DataOutput, ObjectInput, ObjectOutput {
private int pos = 0;
private int count = 0;
private byte[] buf;
public DataInputOutput() {
buf = new byte[8];
}
public DataInputOutput(byte[] data) {
buf = data;
count = data.length;
}
public byte[] getBuf() {
return buf;
}
public int getPos() {
return pos;
}
public void reset() {
pos = 0;
count = 0;
}
public void resetForReading() {
count = pos;
pos = 0;
}
public void reset(byte[] b) {
pos = 0;
buf = b;
count = b.length;
}
public byte[] toByteArray() {
byte[] d = new byte[pos];
System.arraycopy(buf, 0, d, 0, pos);
return d;
}
public int available() {
return count - pos;
}
public void readFully(byte[] b) throws IOException {
readFully(b, 0, b.length);
}
public void readFully(byte[] b, int off, int len) throws IOException {
System.arraycopy(buf, pos, b, off, len);
pos += len;
}
public int skipBytes(int n) throws IOException {
pos += n;
return n;
}
public boolean readBoolean() throws IOException {
return buf[pos++] == 1;
}
public byte readByte() throws IOException {
return buf[pos++];
}
public int readUnsignedByte() throws IOException {
return buf[pos++] & 0xff;
}
public short readShort() throws IOException {
return (short)
(((short) (buf[pos++] & 0xff) << 8) |
((short) (buf[pos++] & 0xff) << 0));
}
public int readUnsignedShort() throws IOException {
return (((int) (buf[pos++] & 0xff) << 8) |
((int) (buf[pos++] & 0xff) << 0));
}
public char readChar() throws IOException {
return (char) readInt();
}
public int readInt() throws IOException {
return
(((buf[pos++] & 0xff) << 24) |
((buf[pos++] & 0xff) << 16) |
((buf[pos++] & 0xff) << 8) |
((buf[pos++] & 0xff) << 0));
}
public long readLong() throws IOException {
return
(((long) (buf[pos++] & 0xff) << 56) |
((long) (buf[pos++] & 0xff) << 48) |
((long) (buf[pos++] & 0xff) << 40) |
((long) (buf[pos++] & 0xff) << 32) |
((long) (buf[pos++] & 0xff) << 24) |
((long) (buf[pos++] & 0xff) << 16) |
((long) (buf[pos++] & 0xff) << 8) |
((long) (buf[pos++] & 0xff) << 0));
}
public float readFloat() throws IOException {
return Float.intBitsToFloat(readInt());
}
public double readDouble() throws IOException {
return Double.longBitsToDouble(readLong());
}
public String readLine() throws IOException {
return readUTF();
}
public String readUTF() throws IOException {
return Serialization.deserializeString(this);
}
/**
* make sure there will be enought space in buffer to write N bytes
*/
private void ensureAvail(int n) {
if (pos + n >= buf.length) {
int newSize = Math.max(pos + n, buf.length * 2);
buf = Arrays.copyOf(buf, newSize);
}
}
public void write(int b) throws IOException {
ensureAvail(1);
buf[pos++] = (byte) b;
}
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
public void write(byte[] b, int off, int len) throws IOException {
ensureAvail(len);
System.arraycopy(b, off, buf, pos, len);
pos += len;
}
public void writeBoolean(boolean v) throws IOException {
ensureAvail(1);
buf[pos++] = (byte) (v ? 1 : 0);
}
public void writeByte(int v) throws IOException {
ensureAvail(1);
buf[pos++] = (byte) (v);
}
public void writeShort(int v) throws IOException {
ensureAvail(2);
buf[pos++] = (byte) (0xff & (v >> 8));
buf[pos++] = (byte) (0xff & (v >> 0));
}
public void writeChar(int v) throws IOException {
writeInt(v);
}
public void writeInt(int v) throws IOException {
ensureAvail(4);
buf[pos++] = (byte) (0xff & (v >> 24));
buf[pos++] = (byte) (0xff & (v >> 16));
buf[pos++] = (byte) (0xff & (v >> 8));
buf[pos++] = (byte) (0xff & (v >> 0));
}
public void writeLong(long v) throws IOException {
ensureAvail(8);
buf[pos++] = (byte) (0xff & (v >> 56));
buf[pos++] = (byte) (0xff & (v >> 48));
buf[pos++] = (byte) (0xff & (v >> 40));
buf[pos++] = (byte) (0xff & (v >> 32));
buf[pos++] = (byte) (0xff & (v >> 24));
buf[pos++] = (byte) (0xff & (v >> 16));
buf[pos++] = (byte) (0xff & (v >> 8));
buf[pos++] = (byte) (0xff & (v >> 0));
}
public void writeFloat(float v) throws IOException {
ensureAvail(4);
writeInt(Float.floatToIntBits(v));
}
public void writeDouble(double v) throws IOException {
ensureAvail(8);
writeLong(Double.doubleToLongBits(v));
}
public void writeBytes(String s) throws IOException {
writeUTF(s);
}
public void writeChars(String s) throws IOException {
writeUTF(s);
}
public void writeUTF(String s) throws IOException {
Serialization.serializeString(this, s);
}
/** helper method to write data directly from PageIo*/
public void writeFromByteBuffer(ByteBuffer b, int offset, int length) {
ensureAvail(length);
b.position(offset);
b.get(buf,pos,length);
pos+=length;
}
//temp var used for Externalizable
SerialClassInfo serializer;
//temp var used for Externalizable
Serialization.FastArrayList objectStack;
public Object readObject() throws ClassNotFoundException, IOException {
//is here just to implement ObjectInput
//Fake method which reads data from serializer.
//We could probably implement separate wrapper for this, but I want to safe class space
return serializer.deserialize(this, objectStack);
}
public int read() throws IOException {
//is here just to implement ObjectInput
return readUnsignedByte();
}
public int read(byte[] b) throws IOException {
//is here just to implement ObjectInput
readFully(b);
return b.length;
}
public int read(byte[] b, int off, int len) throws IOException {
//is here just to implement ObjectInput
readFully(b,off,len);
return len;
}
public long skip(long n) throws IOException {
//is here just to implement ObjectInput
pos += n;
return n;
}
public void close() throws IOException {
//is here just to implement ObjectInput
//do nothing
}
public void writeObject(Object obj) throws IOException {
//is here just to implement ObjectOutput
serializer.serialize(this,obj,objectStack);
}
public void flush() throws IOException {
//is here just to implement ObjectOutput
//do nothing
}
}

View File

@ -0,0 +1,215 @@
///*
//package org.apache.jdbm;
//
//import java.io.DataInput;
//import java.io.DataOutput;
//import java.io.IOException;
//import java.nio.Buffer;
//import java.nio.ByteBuffer;
//import java.util.Arrays;
//
//*/
///**
// * Utility class which implements DataInput and DataOutput on top of ByteBuffer
// * with minimal overhead
// * This class is not used, is left here in case we would ever need it.
// *
// * @author Jan Kotek
// *//*
//
//class DataInputOutput2 implements DataInput, DataOutput {
//
// private ByteBuffer buf;
//
//
// public DataInputOutput2() {
// buf = ByteBuffer.allocate(8);
// }
//
// public DataInputOutput2(ByteBuffer data) {
// buf = data;
// }
//
// public DataInputOutput2(byte[] data) {
// buf = ByteBuffer.wrap(data);
// }
//
//
// public int getPos() {
// return buf.position();
// }
//
//
// public void reset() {
// buf.rewind();
// }
//
//
// public void reset(byte[] b) {
// buf = ByteBuffer.wrap(b);
// }
//
// public void resetForReading() {
// buf.flip();
// }
//
//
// public byte[] toByteArray() {
// byte[] d = new byte[buf.position()];
// buf.position(0);
// buf.get(d); //reading N bytes restores to current position
//
// return d;
// }
//
// public int available() {
// return buf.remaining();
// }
//
//
// public void readFully(byte[] b) throws IOException {
// readFully(b, 0, b.length);
// }
//
// public void readFully(byte[] b, int off, int len) throws IOException {
// buf.get(b,off,len);
// }
//
// public int skipBytes(int n) throws IOException {
// buf.position(buf.position()+n);
// return n;
// }
//
// public boolean readBoolean() throws IOException {
// return buf.get()==1;
// }
//
// public byte readByte() throws IOException {
// return buf.get();
// }
//
// public int readUnsignedByte() throws IOException {
// return buf.get() & 0xff;
// }
//
// public short readShort() throws IOException {
// return buf.getShort();
// }
//
// public int readUnsignedShort() throws IOException {
// return (((int) (buf.get() & 0xff) << 8) |
// ((int) (buf.get() & 0xff) << 0));
// }
//
// public char readChar() throws IOException {
// return (char) readInt();
// }
//
// public int readInt() throws IOException {
// return buf.getInt();
// }
//
// public long readLong() throws IOException {
// return buf.getLong();
// }
//
// public float readFloat() throws IOException {
// return buf.getFloat();
// }
//
// public double readDouble() throws IOException {
// return buf.getDouble();
// }
//
// public String readLine() throws IOException {
// return readUTF();
// }
//
// public String readUTF() throws IOException {
// return Serialization.deserializeString(this);
// }
//
// */
///**
// * make sure there will be enough space in buffer to write N bytes
// *//*
//
// private void ensureAvail(int n) {
// int pos = buf.position();
// if (pos + n >= buf.limit()) {
// int newSize = Math.max(pos + n, buf.limit() * 2);
// byte[] b = new byte[newSize];
// buf.get(b);
// buf = ByteBuffer.wrap(b);
// buf.position(pos);
// }
// }
//
//
// public void write(final int b) throws IOException {
// ensureAvail(1);
// buf.put((byte) b);
// }
//
// public void write(final byte[] b) throws IOException {
// write(b, 0, b.length);
// }
//
// public void write(final byte[] b, final int off, final int len) throws IOException {
// ensureAvail(len);
// buf.put(b,off,len);
// }
//
// public void writeBoolean(final boolean v) throws IOException {
// ensureAvail(1);
// buf.put((byte) (v?1:0));
// }
//
// public void writeByte(final int v) throws IOException {
// ensureAvail(1);
// buf.put((byte) v);
// }
//
// public void writeShort(final short v) throws IOException {
// ensureAvail(2);
// buf.putShort(v);
// }
//
// public void writeChar(final int v) throws IOException {
// writeInt(v);
// }
//
// public void writeInt(final int v) throws IOException {
// ensureAvail(4);
// buf.putInt(v);
// }
//
// public void writeLong(final long v) throws IOException {
// ensureAvail(8);
// buf.putLong(v);
// }
//
// public void writeFloat(final float v) throws IOException {
// ensureAvail(4);
// buf.putFloat(v);
// }
//
// public void writeDouble(final double v) throws IOException {
// ensureAvail(8);
// buf.putDouble(v);
// }
//
// public void writeBytes(String s) throws IOException {
// writeUTF(s);
// }
//
// public void writeChars(String s) throws IOException {
// writeUTF(s);
// }
//
// public void writeUTF(String s) throws IOException {
// Serialization.serializeString(this, s);
// }
//
//}
//*/

View File

@ -0,0 +1,542 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Persistent HashMap implementation for DB.
* Implemented as an H*Tree structure.
*
* @author Alex Boisvert
* @author Jan Kotek
*/
class HTree<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
final Serializer SERIALIZER = new Serializer<Object>() {
public Object deserialize(DataInput ds2) throws IOException {
DataInputOutput ds = (DataInputOutput) ds2;
try {
int i = ds.readUnsignedByte();
if (i == SerializationHeader.HTREE_BUCKET) { //is HashBucket?
HTreeBucket ret = new HTreeBucket(HTree.this);
if (loadValues)
ret.readExternal(ds);
if (loadValues && ds.available() != 0)
throw new InternalError("bytes left: " + ds.available());
return ret;
} else if (i == SerializationHeader.HTREE_DIRECTORY) {
HTreeDirectory ret = new HTreeDirectory(HTree.this);
ret.readExternal(ds);
if (loadValues && ds.available() != 0)
throw new InternalError("bytes left: " + ds.available());
return ret;
} else {
throw new InternalError("Wrong HTree header: " + i);
}
} catch (ClassNotFoundException e) {
throw new IOException(e);
}
}
public void serialize(DataOutput out, Object obj) throws IOException {
if (obj instanceof HTreeBucket) {
out.write(SerializationHeader.HTREE_BUCKET);
HTreeBucket b = (HTreeBucket) obj;
b.writeExternal(out);
} else {
out.write(SerializationHeader.HTREE_DIRECTORY);
HTreeDirectory n = (HTreeDirectory) obj;
n.writeExternal(out);
}
}
};
final protected ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* Listeners which are notified about changes in records
*/
protected RecordListener[] recordListeners = new RecordListener[0];
/**
* Serializer used to serialize index keys (optional)
*/
protected Serializer<K> keySerializer;
/**
* Serializer used to serialize index values (optional)
*/
protected Serializer<V> valueSerializer;
protected boolean readonly = false;
final long rootRecid;
DBAbstract db;
/** if false map contains only keys, used for set*/
boolean hasValues = true;
/**
* counts structural changes in tree at runtume. Is here to support fail-fast behaviour.
*/
int modCount;
/**
* indicates if values should be loaded during deserialization, set to true during defragmentation
*/
private boolean loadValues = true;
public Serializer<K> getKeySerializer() {
return keySerializer;
}
public Serializer<V> getValueSerializer() {
return valueSerializer;
}
/**
* cache writing buffer, so it does not have to be allocated on each write
*/
AtomicReference<DataInputOutput> writeBufferCache = new AtomicReference<DataInputOutput>();
/**
* Create a persistent hashtable.
*/
public HTree(DBAbstract db, Serializer<K> keySerializer, Serializer<V> valueSerializer, boolean hasValues)
throws IOException {
this.keySerializer = keySerializer;
this.valueSerializer = valueSerializer;
this.db = db;
this.hasValues = hasValues;
HTreeDirectory<K, V> root = new HTreeDirectory<K, V>(this, (byte) 0);
root.setPersistenceContext(0);
this.rootRecid = db.insert(root, this.SERIALIZER,false);
}
/**
* Load a persistent hashtable
*/
public HTree(DBAbstract db,long rootRecid, Serializer<K> keySerializer, Serializer<V> valueSerializer, boolean hasValues)
throws IOException {
this.db = db;
this.rootRecid = rootRecid;
this.keySerializer = keySerializer;
this.valueSerializer = valueSerializer;
this.hasValues = hasValues;
}
void setPersistenceContext(DBAbstract db) {
this.db = db;
}
public V put(K key, V value) {
if (readonly)
throw new UnsupportedOperationException("readonly");
lock.writeLock().lock();
try {
if (key == null || value == null)
throw new NullPointerException("Null key or value");
V oldVal = (V) getRoot().put(key, value);
if (oldVal == null) {
modCount++;
//increase size
HTreeDirectory root = getRoot();
root.size++;
db.update(rootRecid,root,SERIALIZER);
for (RecordListener<K, V> r : recordListeners)
r.recordInserted(key, value);
} else {
//notify listeners
for (RecordListener<K, V> r : recordListeners)
r.recordUpdated(key, oldVal, value);
}
return oldVal;
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.writeLock().unlock();
}
}
public V get(Object key) {
if (key == null)
return null;
lock.readLock().lock();
try {
return getRoot().get((K) key);
} catch (ClassCastException e) {
return null;
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.readLock().unlock();
}
}
public V remove(Object key) {
if (readonly)
throw new UnsupportedOperationException("readonly");
lock.writeLock().lock();
try {
if (key == null)
return null;
V val = (V) getRoot().remove(key);
modCount++;
if (val != null){
//decrease size
HTreeDirectory root = getRoot();
root.size--;
db.update(rootRecid,root,SERIALIZER);
for (RecordListener r : recordListeners)
r.recordRemoved(key, val);
}
return val;
} catch (ClassCastException e) {
return null;
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.writeLock().unlock();
}
}
public boolean containsKey(Object key) {
if (key == null)
return false;
//no need for locking, get is already locked
V v = get((K) key);
return v != null;
}
public void clear() {
lock.writeLock().lock();
try {
Iterator<K> keyIter = keys();
while (keyIter.hasNext()) {
keyIter.next();
keyIter.remove();
}
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.writeLock().unlock();
}
}
/**
* Returns an enumeration of the keys contained in this
*/
public Iterator<K> keys()
throws IOException {
lock.readLock().lock();
try{
return getRoot().keys();
}finally {
lock.readLock().unlock();
}
}
public DBAbstract getRecordManager() {
return db;
}
/**
* add RecordListener which is notified about record changes
*
* @param listener
*/
public void addRecordListener(RecordListener<K, V> listener) {
recordListeners = Arrays.copyOf(recordListeners, recordListeners.length + 1);
recordListeners[recordListeners.length - 1] = listener;
}
/**
* remove RecordListener which is notified about record changes
*
* @param listener
*/
public void removeRecordListener(RecordListener<K, V> listener) {
List l = Arrays.asList(recordListeners);
l.remove(listener);
recordListeners = (RecordListener[]) l.toArray(new RecordListener[1]);
}
public Set<Entry<K, V>> entrySet() {
return _entrySet;
}
private Set<Entry<K, V>> _entrySet = new AbstractSet<Entry<K, V>>() {
protected Entry<K, V> newEntry(K k, V v) {
return new SimpleEntry<K, V>(k, v) {
private static final long serialVersionUID = 978651696969194154L;
public V setValue(V arg0) {
//put is already locked
HTree.this.put(getKey(), arg0);
return super.setValue(arg0);
}
};
}
public boolean add(java.util.Map.Entry<K, V> e) {
if (readonly)
throw new UnsupportedOperationException("readonly");
if (e.getKey() == null)
throw new NullPointerException("Can not add null key");
lock.writeLock().lock();
try{
if (e.getValue().equals(get(e.getKey())))
return false;
HTree.this.put(e.getKey(), e.getValue());
return true;
}finally {
lock.writeLock().unlock();
}
}
@SuppressWarnings("unchecked")
public boolean contains(Object o) {
if (o instanceof Entry) {
Entry<K, V> e = (java.util.Map.Entry<K, V>) o;
//get is already locked
if (e.getKey() != null && HTree.this.get(e.getKey()) != null)
return true;
}
return false;
}
public Iterator<java.util.Map.Entry<K, V>> iterator() {
try {
final Iterator<K> br = keys();
return new Iterator<Entry<K, V>>() {
public boolean hasNext() {
return br.hasNext();
}
public java.util.Map.Entry<K, V> next() {
K k = br.next();
return newEntry(k, get(k));
}
public void remove() {
if (readonly)
throw new UnsupportedOperationException("readonly");
br.remove();
}
};
} catch (IOException e) {
throw new IOError(e);
}
}
@SuppressWarnings("unchecked")
public boolean remove(Object o) {
if (readonly)
throw new UnsupportedOperationException("readonly");
if (o instanceof Entry) {
Entry<K, V> e = (java.util.Map.Entry<K, V>) o;
//check for nulls
if (e.getKey() == null || e.getValue() == null)
return false;
lock.writeLock().lock();
try{
//get old value, must be same as item in entry
V v = get(e.getKey());
if (v == null || !e.getValue().equals(v))
return false;
HTree.this.remove(e.getKey());
return true;
}finally{
lock.writeLock().unlock();
}
}
return false;
}
@Override
public int size() {
lock.readLock().lock();
try {
int counter = 0;
Iterator<K> it = keys();
while (it.hasNext()) {
it.next();
counter++;
}
return counter;
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.readLock().unlock();
}
}
};
HTreeDirectory<K, V> getRoot() {
//assumes that caller already holds read or write lock
try {
HTreeDirectory<K, V> root = (HTreeDirectory<K, V>) db.fetch(rootRecid, this.SERIALIZER);
root.setPersistenceContext(rootRecid);
return root;
} catch (IOException e) {
throw new IOError(e);
}
}
public static HTree deserialize(DataInput is, Serialization ser) throws IOException, ClassNotFoundException {
long rootRecid = LongPacker.unpackLong(is);
boolean hasValues = is.readBoolean();
Serializer keySerializer = (Serializer) ser.deserialize(is);
Serializer valueSerializer = (Serializer) ser.deserialize(is);
return new HTree(ser.db,rootRecid, keySerializer, valueSerializer, hasValues);
}
void serialize(DataOutput out) throws IOException {
LongPacker.packLong(out, rootRecid);
out.writeBoolean(hasValues);;
db.defaultSerializer().serialize(out, keySerializer);
db.defaultSerializer().serialize(out, valueSerializer);
}
static void defrag(Long recid, DBStore r1, DBStore r2) throws IOException {
//TODO should modCount be increased after defrag, revert or commit?
try {
byte[] data = r1.fetchRaw(recid);
r2.forceInsert(recid, data);
DataInput in = new DataInputStream(new ByteArrayInputStream(data));
HTree t = (HTree) r1.defaultSerializer().deserialize(in);
t.db = r1;
t.loadValues = false;
HTreeDirectory d = t.getRoot();
if (d != null) {
r2.forceInsert(t.rootRecid, r1.fetchRaw(t.rootRecid));
d.defrag(r1, r2);
}
} catch (ClassNotFoundException e) {
throw new IOError(e);
}
}
public int size(){
return (int) getRoot().size;
}
public boolean hasValues() {
return hasValues;
}
public V putIfAbsent(K key, V value) {
lock.writeLock().lock();
try{
if (!containsKey(key))
return put(key, value);
else
return get(key);
}finally {
lock.writeLock().unlock();
}
}
public boolean remove(Object key, Object value) {
lock.writeLock().lock();
try{
if (containsKey(key) && get(key).equals(value)) {
remove(key);
return true;
} else return false;
}finally {
lock.writeLock().unlock();
}
}
public boolean replace(K key, V oldValue, V newValue) {
lock.writeLock().lock();
try{
if (containsKey(key) && get(key).equals(oldValue)) {
put(key, newValue);
return true;
} else return false;
}finally {
lock.writeLock().unlock();
}
}
public V replace(K key, V value) {
lock.writeLock().lock();
try{
if (containsKey(key)) {
return put(key, value);
} else return null;
}finally {
lock.writeLock().unlock();
}
}
}

View File

@ -0,0 +1,352 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.*;
import java.util.ArrayList;
/**
* A bucket is a placeholder for multiple (key, value) pairs. Buckets
* are used to store collisions (same hash value) at all levels of an
* H*tree.
* <p/>
* There are two types of buckets: leaf and non-leaf.
* <p/>
* Non-leaf buckets are buckets which hold collisions which happen
* when the H*tree is not fully expanded. Keys in a non-leaf buckets
* can have different hash codes. Non-leaf buckets are limited to an
* arbitrary size. When this limit is reached, the H*tree should create
* a new HTreeDirectory node and distribute keys of the non-leaf buckets into
* the newly created HTreeDirectory.
* <p/>
* A leaf bucket is a bucket which contains keys which all have
* the same <code>hashCode()</code>. Leaf buckets stand at the
* bottom of an H*tree because the hashing algorithm cannot further
* discriminate between different keys based on their hash code.
*
* @author Alex Boisvert
*/
final class HTreeBucket<K, V> {
/**
* The maximum number of elements (key, value) a non-leaf bucket
* can contain.
*/
public static final int OVERFLOW_SIZE = 16;
/**
* Depth of this bucket.
*/
private byte _depth;
/**
* Keys and values in this bucket. Keys are followed by values at KEYPOS+OVERFLOW_SIZE
*/
private Object[] _keysAndValues;
private byte size = 0;
private final HTree<K, V> tree;
/**
* Public constructor for serialization.
*/
public HTreeBucket(HTree<K, V> tree) {
this.tree = tree;
}
/**
* Construct a bucket with a given depth level. Depth level is the
* number of <code>HashDirectory</code> above this bucket.
*/
public HTreeBucket(HTree<K, V> tree, byte level) {
this.tree = tree;
if (level > HTreeDirectory.MAX_DEPTH + 1) {
throw new IllegalArgumentException(
"Cannot create bucket with depth > MAX_DEPTH+1. "
+ "Depth=" + level);
}
_depth = level;
_keysAndValues = new Object[OVERFLOW_SIZE * 2];
}
/**
* Returns the number of elements contained in this bucket.
*/
public int getElementCount() {
return size;
}
/**
* Returns whether or not this bucket is a "leaf bucket".
*/
public boolean isLeaf() {
return (_depth > HTreeDirectory.MAX_DEPTH);
}
/**
* Returns true if bucket can accept at least one more element.
*/
public boolean hasRoom() {
if (isLeaf()) {
return true; // leaf buckets are never full
} else {
// non-leaf bucket
return (size < OVERFLOW_SIZE);
}
}
/**
* Add an element (key, value) to this bucket. If an existing element
* has the same key, it is replaced silently.
*
* @return Object which was previously associated with the given key
* or <code>null</code> if no association existed.
*/
public V addElement(K key, V value) {
//find entry
byte existing = -1;
for (byte i = 0; i < size; i++) {
if (key.equals(_keysAndValues[i])) {
existing = i;
break;
}
}
if (existing != -1) {
// replace existing element
Object before = _keysAndValues[existing + OVERFLOW_SIZE];
if (before instanceof BTreeLazyRecord) {
BTreeLazyRecord<V> rec = (BTreeLazyRecord<V>) before;
before = rec.get();
rec.delete();
}
_keysAndValues[existing + OVERFLOW_SIZE] = value;
return (V) before;
} else {
// add new (key, value) pair
_keysAndValues[size] = key;
_keysAndValues[size + OVERFLOW_SIZE] = value;
size++;
return null;
}
}
/**
* Remove an element, given a specific key.
*
* @param key Key of the element to remove
* @return Removed element value, or <code>null</code> if not found
*/
public V removeElement(K key) {
//find entry
byte existing = -1;
for (byte i = 0; i < size; i++) {
if (key.equals(_keysAndValues[i])) {
existing = i;
break;
}
}
if (existing != -1) {
Object o = _keysAndValues[existing + OVERFLOW_SIZE];
if (o instanceof BTreeLazyRecord) {
BTreeLazyRecord<V> rec = (BTreeLazyRecord<V>) o;
o = rec.get();
rec.delete();
}
//move last element to existing
size--;
_keysAndValues[existing] = _keysAndValues[size];
_keysAndValues[existing + OVERFLOW_SIZE] = _keysAndValues[size + OVERFLOW_SIZE];
//and unset last element
_keysAndValues[size] = null;
_keysAndValues[size + OVERFLOW_SIZE] = null;
return (V) o;
} else {
// not found
return null;
}
}
/**
* Returns the value associated with a given key. If the given key
* is not found in this bucket, returns <code>null</code>.
*/
public V getValue(K key) {
//find entry
byte existing = -1;
for (byte i = 0; i < size; i++) {
if (key.equals(_keysAndValues[i])) {
existing = i;
break;
}
}
if (existing != -1) {
Object o = _keysAndValues[existing + OVERFLOW_SIZE];
if (o instanceof BTreeLazyRecord)
return ((BTreeLazyRecord<V>) o).get();
else
return (V) o;
} else {
// key not found
return null;
}
}
/**
* Obtain keys contained in this buckets. Keys are ordered to match
* their values, which be be obtained by calling <code>getValues()</code>.
* <p/>
* As an optimization, the Vector returned is the instance member
* of this class. Please don't modify outside the scope of this class.
*/
ArrayList<K> getKeys() {
ArrayList<K> ret = new ArrayList<K>();
for (byte i = 0; i < size; i++) {
ret.add((K) _keysAndValues[i]);
}
return ret;
}
/**
* Obtain values contained in this buckets. Values are ordered to match
* their keys, which be be obtained by calling <code>getKeys()</code>.
* <p/>
* As an optimization, the Vector returned is the instance member
* of this class. Please don't modify outside the scope of this class.
*/
ArrayList<V> getValues() {
ArrayList<V> ret = new ArrayList<V>();
for (byte i = 0; i < size; i++) {
ret.add((V) _keysAndValues[i + OVERFLOW_SIZE]);
}
return ret;
}
public void writeExternal(DataOutput out)
throws IOException {
out.write(_depth);
out.write(size);
DataInputOutput out3 = tree.writeBufferCache.getAndSet(null);
if (out3 == null)
out3 = new DataInputOutput();
else
out3.reset();
Serializer keySerializer = tree.keySerializer != null ? tree.keySerializer : tree.getRecordManager().defaultSerializer();
for (byte i = 0; i < size; i++) {
out3.reset();
keySerializer.serialize(out3, _keysAndValues[i]);
LongPacker.packInt(out, out3.getPos());
out.write(out3.getBuf(), 0, out3.getPos());
}
//write values
if(tree.hasValues()){
Serializer valSerializer = tree.valueSerializer != null ? tree.valueSerializer : tree.getRecordManager().defaultSerializer();
for (byte i = 0; i < size; i++) {
Object value = _keysAndValues[i + OVERFLOW_SIZE];
if (value == null) {
out.write(BTreeLazyRecord.NULL);
} else if (value instanceof BTreeLazyRecord) {
out.write(BTreeLazyRecord.LAZY_RECORD);
LongPacker.packLong(out, ((BTreeLazyRecord) value).recid);
} else {
//transform to byte array
out3.reset();
valSerializer.serialize(out3, value);
if (out3.getPos() > BTreeLazyRecord.MAX_INTREE_RECORD_SIZE) {
//store as separate record
long recid = tree.getRecordManager().insert(out3.toByteArray(), BTreeLazyRecord.FAKE_SERIALIZER,true);
out.write(BTreeLazyRecord.LAZY_RECORD);
LongPacker.packLong(out, recid);
} else {
out.write(out3.getPos());
out.write(out3.getBuf(), 0, out3.getPos());
}
}
}
}
tree.writeBufferCache.set(out3);
}
public void readExternal(DataInputOutput in) throws IOException, ClassNotFoundException {
_depth = in.readByte();
size = in.readByte();
//read keys
Serializer keySerializer = tree.keySerializer != null ? tree.keySerializer : tree.getRecordManager().defaultSerializer();
_keysAndValues = (K[]) new Object[OVERFLOW_SIZE * 2];
for (byte i = 0; i < size; i++) {
int expectedSize = LongPacker.unpackInt(in);
K key = (K) BTreeLazyRecord.fastDeser(in, keySerializer, expectedSize);
_keysAndValues[i] = key;
}
//read values
if(tree.hasValues()){
Serializer<V> valSerializer = tree.valueSerializer != null ? tree.valueSerializer : (Serializer<V>) tree.getRecordManager().defaultSerializer();
for (byte i = 0; i < size; i++) {
int header = in.readUnsignedByte();
if (header == BTreeLazyRecord.NULL) {
_keysAndValues[i + OVERFLOW_SIZE] = null;
} else if (header == BTreeLazyRecord.LAZY_RECORD) {
long recid = LongPacker.unpackLong(in);
_keysAndValues[i + OVERFLOW_SIZE] = (new BTreeLazyRecord(tree.getRecordManager(), recid, valSerializer));
} else {
_keysAndValues[i + OVERFLOW_SIZE] = BTreeLazyRecord.fastDeser(in, valSerializer, header);
}
}
}else{
for (byte i = 0; i < size; i++) {
if(_keysAndValues[i]!=null)
_keysAndValues[i+OVERFLOW_SIZE] = Utils.EMPTY_STRING;
}
}
}
}

View File

@ -0,0 +1,618 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.*;
import java.util.*;
/**
* Hashtable directory page.
*
* @author Alex Boisvert
*/
final class HTreeDirectory<K, V> {
/**
* Maximum number of children in a directory.
* <p/>
* (Must be a power of 2 -- if you update this value, you must also
* update BIT_SIZE and MAX_DEPTH.)
* <p/>
* !!!! do not change this, it affects storage format, there are also magic numbers which relies on 255 !!!
*/
static final int MAX_CHILDREN = 256;
/**
* Number of significant bits per directory level.
*/
static final int BIT_SIZE = 8; // log2(256) = 8
/**
* Maximum number of levels (zero-based)
* <p/>
* (4 * 8 bits = 32 bits, which is the size of an "int", and as
* you know, hashcodes in Java are "ints")
*/
static final int MAX_DEPTH = 3; // 4 levels
/**
* Record ids of children nodes.
* It is saved in matrix to save memory, some subarrays may be null.
*/
private long[][] _children;
/**
* Depth of this directory page, zero-based
*/
private byte _depth;
/**
* This directory's record ID in the DB. (transient)
*/
private long _recid;
/** if this is root (depth=0), it contains size, otherwise -1*/
long size;
protected final HTree<K, V> tree;
/**
* Public constructor used by serialization
*/
public HTreeDirectory(HTree<K, V> tree) {
this.tree = tree;
}
/**
* Construct a HashDirectory
*
* @param depth Depth of this directory node.
*/
HTreeDirectory(HTree<K, V> tree, byte depth) {
this.tree = tree;
_depth = depth;
_children = new long[32][];
}
/**
* Sets persistence context. This method must be called before any
* persistence-related operation.
*
* @param recid Record id of this directory.
*/
void setPersistenceContext(long recid) {
this._recid = recid;
}
/**
* Get the record identifier used to load this hashtable.
*/
long getRecid() {
return _recid;
}
/**
* Returns whether or not this directory is empty. A directory
* is empty when it no longer contains buckets or sub-directories.
*/
boolean isEmpty() {
for (int i = 0; i < _children.length; i++) {
long[] sub = _children[i];
if (sub!=null){
for (int j = 0; j < 8; j++) {
if(sub[j] != 0) {
return false;
}
}
}
}
return true;
}
/**
* Returns the value which is associated with the given key. Returns
* <code>null</code> if there is not association for this key.
*
* @param key key whose associated value is to be returned
*/
V get(K key)
throws IOException {
int hash = hashCode(key);
long child_recid = getRecid(hash);
if (child_recid == 0) {
// not bucket/node --> not found
return null;
} else {
Object node = tree.db.fetch(child_recid, tree.SERIALIZER);
// System.out.println("HashDirectory.get() child is : "+node);
if (node instanceof HTreeDirectory) {
// recurse into next directory level
HTreeDirectory<K, V> dir = (HTreeDirectory<K, V>) node;
dir.setPersistenceContext(child_recid);
return dir.get(key);
} else {
// node is a bucket
HTreeBucket<K, V> bucket = (HTreeBucket) node;
return bucket.getValue(key);
}
}
}
private long getRecid(int hash) {
long[] sub = _children[hash>>>3];
return sub==null? 0 : sub[hash%8];
}
private void putRecid(int hash, long recid) {
long[] sub = _children[hash>>>3];
if(sub == null){
sub = new long[8];
_children[hash>>>3] = sub;
}
sub[hash%8] = recid;
}
/**
* Associates the specified value with the specified key.
*
* @param key key with which the specified value is to be assocated.
* @param value value to be associated with the specified key.
* @return object which was previously associated with the given key,
* or <code>null</code> if no association existed.
*/
Object put(final Object key, final Object value)
throws IOException {
if (value == null) {
return remove(key);
}
int hash = hashCode(key);
long child_recid = getRecid(hash);
if (child_recid == 0) {
// no bucket/node here yet, let's create a bucket
HTreeBucket bucket = new HTreeBucket(tree, (byte) (_depth + 1));
// insert (key,value) pair in bucket
Object existing = bucket.addElement(key, value);
long b_recid = tree.db.insert(bucket, tree.SERIALIZER,false);
putRecid(hash, b_recid);
tree.db.update(_recid, this, tree.SERIALIZER);
// System.out.println("Added: "+bucket);
return existing;
} else {
Object node = tree.db.fetch(child_recid, tree.SERIALIZER);
if (node instanceof HTreeDirectory) {
// recursive insert in next directory level
HTreeDirectory dir = (HTreeDirectory) node;
dir.setPersistenceContext(child_recid);
return dir.put(key, value);
} else {
// node is a bucket
HTreeBucket bucket = (HTreeBucket) node;
if (bucket.hasRoom()) {
Object existing = bucket.addElement(key, value);
tree.db.update(child_recid, bucket, tree.SERIALIZER);
// System.out.println("Added: "+bucket);
return existing;
} else {
// overflow, so create a new directory
if (_depth == MAX_DEPTH) {
throw new RuntimeException("Cannot create deeper directory. "
+ "Depth=" + _depth);
}
HTreeDirectory dir = new HTreeDirectory(tree, (byte) (_depth + 1));
long dir_recid = tree.db.insert(dir, tree.SERIALIZER,false);
dir.setPersistenceContext(dir_recid);
putRecid(hash, dir_recid);
tree.db.update(_recid, this, tree.SERIALIZER);
// discard overflown bucket
tree.db.delete(child_recid);
// migrate existing bucket elements
ArrayList keys = bucket.getKeys();
ArrayList values = bucket.getValues();
int entries = keys.size();
for (int i = 0; i < entries; i++) {
dir.put(keys.get(i), values.get(i));
}
// (finally!) insert new element
return dir.put(key, value);
}
}
}
}
/**
* Remove the value which is associated with the given key. If the
* key does not exist, this method simply ignores the operation.
*
* @param key key whose associated value is to be removed
* @return object which was associated with the given key, or
* <code>null</code> if no association existed with given key.
*/
Object remove(Object key) throws IOException {
int hash = hashCode(key);
long child_recid = getRecid(hash);
if (child_recid == 0) {
// not bucket/node --> not found
return null;
} else {
Object node = tree.db.fetch(child_recid, tree.SERIALIZER);
// System.out.println("HashDirectory.remove() child is : "+node);
if (node instanceof HTreeDirectory) {
// recurse into next directory level
HTreeDirectory dir = (HTreeDirectory) node;
dir.setPersistenceContext(child_recid);
Object existing = dir.remove(key);
if (existing != null) {
if (dir.isEmpty()) {
// delete empty directory
tree.db.delete(child_recid);
putRecid(hash, 0);
tree.db.update(_recid, this, tree.SERIALIZER);
}
}
return existing;
} else {
// node is a bucket
HTreeBucket bucket = (HTreeBucket) node;
Object existing = bucket.removeElement(key);
if (existing != null) {
if (bucket.getElementCount() >= 1) {
tree.db.update(child_recid, bucket, tree.SERIALIZER);
} else {
// delete bucket, it's empty
tree.db.delete(child_recid);
putRecid(hash, 0);
tree.db.update(_recid, this, tree.SERIALIZER);
}
}
return existing;
}
}
}
/**
* Calculates the hashcode of a key, based on the current directory
* depth.
*/
private int hashCode(Object key) {
int hashMask = hashMask();
int hash = key.hashCode();
hash = hash & hashMask;
hash = hash >>> ((MAX_DEPTH - _depth) * BIT_SIZE);
hash = hash % MAX_CHILDREN;
/*
System.out.println("HashDirectory.hashCode() is: 0x"
+Integer.toHexString(hash)
+" for object hashCode() 0x"
+Integer.toHexString(key.hashCode()));
*/
return hash;
}
/**
* Calculates the hashmask of this directory. The hashmask is the
* bit mask applied to a hashcode to retain only bits that are
* relevant to this directory level.
*/
int hashMask() {
int bits = MAX_CHILDREN - 1;
int hashMask = bits << ((MAX_DEPTH - _depth) * BIT_SIZE);
/*
System.out.println("HashDirectory.hashMask() is: 0x"
+Integer.toHexString(hashMask));
*/
return hashMask;
}
/**
* Returns an enumeration of the keys contained in this
*/
Iterator<K> keys()
throws IOException {
return new HDIterator(true);
}
/**
* Returns an enumeration of the values contained in this
*/
Iterator<V> values()
throws IOException {
return new HDIterator(false);
}
public void writeExternal(DataOutput out)
throws IOException {
out.writeByte(_depth);
if(_depth==0){
LongPacker.packLong(out,size);
}
int zeroStart = 0;
for (int i = 0; i < MAX_CHILDREN; i++) {
if (getRecid(i) != 0) {
zeroStart = i;
break;
}
}
out.write(zeroStart);
if (zeroStart == MAX_CHILDREN)
return;
int zeroEnd = 0;
for (int i = MAX_CHILDREN - 1; i >= 0; i--) {
if (getRecid(i) != 0) {
zeroEnd = i;
break;
}
}
out.write(zeroEnd);
for (int i = zeroStart; i <= zeroEnd; i++) {
LongPacker.packLong(out, getRecid(i));
}
}
public void readExternal(DataInputOutput in)
throws IOException, ClassNotFoundException {
_depth = in.readByte();
if(_depth==0)
size = LongPacker.unpackLong(in);
else
size = -1;
_children = new long[32][];
int zeroStart = in.readUnsignedByte();
int zeroEnd = in.readUnsignedByte();
for (int i = zeroStart; i <= zeroEnd; i++) {
long recid = LongPacker.unpackLong(in);
if(recid!=0)
putRecid(i,recid);
}
}
public void defrag(DBStore r1, DBStore r2) throws IOException, ClassNotFoundException {
for (long[] sub: _children) {
if(sub==null) continue;
for (long child : sub) {
if (child == 0) continue;
byte[] data = r1.fetchRaw(child);
r2.forceInsert(child, data);
Object t = tree.SERIALIZER.deserialize(new DataInputOutput(data));
if (t instanceof HTreeDirectory) {
((HTreeDirectory) t).defrag(r1, r2);
}
}
}
}
void deleteAllChildren() throws IOException {
for(long[] ll : _children){
if(ll!=null){
for(long l:ll ){
if(l!=0){
tree.db.delete(l);
}
}
}
}
}
////////////////////////////////////////////////////////////////////////
// INNER CLASS
////////////////////////////////////////////////////////////////////////
/**
* Utility class to enumerate keys/values in a HTree
*/
class HDIterator<A> implements Iterator<A> {
/**
* True if we're iterating on keys, False if enumerating on values.
*/
private boolean _iterateKeys;
/**
* Stacks of directories & last enumerated child position
*/
private ArrayList _dirStack;
private ArrayList _childStack;
/**
* Current HashDirectory in the hierarchy
*/
private HTreeDirectory _dir;
/**
* Current child position
*/
private int _child;
/**
* Current bucket iterator
*/
private Iterator<A> _iter;
private A next;
/**
* last item returned in next(), is used to remove() last item
*/
private A last;
private int expectedModCount;
/**
* Construct an iterator on this directory.
*
* @param iterateKeys True if iteration supplies keys, False
* if iterateKeys supplies values.
*/
HDIterator(boolean iterateKeys)
throws IOException {
_dirStack = new ArrayList();
_childStack = new ArrayList();
_dir = HTreeDirectory.this;
_child = -1;
_iterateKeys = iterateKeys;
expectedModCount = tree.modCount;
prepareNext();
next = next2();
}
/**
* Returns the next object.
*/
public A next2() {
A next = null;
if (_iter != null && _iter.hasNext()) {
next = _iter.next();
} else {
try {
prepareNext();
} catch (IOException except) {
throw new IOError(except);
}
if (_iter != null && _iter.hasNext()) {
return next2();
}
}
return next;
}
/**
* Prepare internal state so we can answer <code>hasMoreElements</code>
* <p/>
* Actually, this code prepares an Enumeration on the next
* Bucket to enumerate. If no following bucket is found,
* the next Enumeration is set to <code>null</code>.
*/
private void prepareNext() throws IOException {
long child_recid = 0;
// get next bucket/directory to enumerate
do {
_child++;
if (_child >= MAX_CHILDREN) {
if (_dirStack.isEmpty()) {
// no more directory in the stack, we're finished
return;
}
// try next node
_dir = (HTreeDirectory) _dirStack.remove(_dirStack.size() - 1);
_child = ((Integer) _childStack.remove(_childStack.size() - 1)).intValue();
continue;
}
child_recid = _dir.getRecid(_child);
} while (child_recid == 0);
if (child_recid == 0) {
throw new Error("child_recid cannot be 0");
}
Object node = tree.db.fetch(child_recid, tree.SERIALIZER);
// System.out.println("HDEnumeration.get() child is : "+node);
if (node instanceof HTreeDirectory) {
// save current position
_dirStack.add(_dir);
_childStack.add(new Integer(_child));
_dir = (HTreeDirectory) node;
_child = -1;
// recurse into
_dir.setPersistenceContext(child_recid);
prepareNext();
} else {
// node is a bucket
HTreeBucket bucket = (HTreeBucket) node;
if (_iterateKeys) {
ArrayList keys2 = bucket.getKeys();
_iter = keys2.iterator();
} else {
_iter = bucket.getValues().iterator();
}
}
}
public boolean hasNext() {
return next != null;
}
public A next() {
if (next == null) throw new NoSuchElementException();
if (expectedModCount != tree.modCount)
throw new ConcurrentModificationException();
last = next;
next = next2();
return last;
}
public void remove() {
if (last == null) throw new IllegalStateException();
if (expectedModCount != tree.modCount)
throw new ConcurrentModificationException();
//TODO current delete behaviour may change node layout. INVESTIGATE if this can happen!
tree.remove(last);
last = null;
expectedModCount++;
}
}
}

View File

@ -0,0 +1,47 @@
package org.apache.jdbm;
import java.util.AbstractSet;
import java.util.Iterator;
/**
* Wrapper for HTree to implement java.util.Map interface
*/
class HTreeSet<E> extends AbstractSet<E> {
final HTree<E, Object> map;
HTreeSet(HTree map) {
this.map = map;
}
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, Utils.EMPTY_STRING) == null;
}
public boolean remove(Object o) {
return map.remove(o) == Utils.EMPTY_STRING;
}
public void clear() {
map.clear();
}
}

View File

@ -0,0 +1,480 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jdbm;
import java.io.*;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* LinkedList2 which stores its nodes on disk.
*
* @author Jan Kotek
*/
class LinkedList2<E> extends AbstractSequentialList<E> {
private DBAbstract db;
final long rootRecid;
/** size limit, is not currently used, but needs to be here for future compatibility.
* Zero means no limit.
*/
long sizeLimit = 0;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
static final class Root{
long first;
long last;
long size;
}
private static final Serializer<Root> ROOT_SERIALIZER= new Serializer<Root>(){
public void serialize(DataOutput out, Root obj) throws IOException {
LongPacker.packLong(out,obj.first);
LongPacker.packLong(out,obj.last);
LongPacker.packLong(out,obj.size);
}
public Root deserialize(DataInput in) throws IOException, ClassNotFoundException {
Root r = new Root();
r.first = LongPacker.unpackLong(in);
r.last = LongPacker.unpackLong(in);
r.size = LongPacker.unpackLong(in);
return r;
}
};
private Serializer<E> valueSerializer;
/**
* indicates that entry values should not be loaded during deserialization, used during defragmentation
*/
protected boolean loadValues = true;
/** constructor used for deserialization */
LinkedList2(DBAbstract db,long rootRecid, Serializer<E> valueSerializer) {
this.db = db;
this.rootRecid = rootRecid;
this.valueSerializer = valueSerializer;
}
/** constructor used to create new empty list*/
LinkedList2(DBAbstract db, Serializer<E> valueSerializer) throws IOException {
this.db = db;
if (valueSerializer != null && !(valueSerializer instanceof Serializable))
throw new IllegalArgumentException("Serializer does not implement Serializable");
this.valueSerializer = valueSerializer;
//create root
this.rootRecid = db.insert(new Root(), ROOT_SERIALIZER,false);
}
void setPersistenceContext(DBAbstract db) {
this.db = db;
}
public ListIterator<E> listIterator(int index) {
lock.readLock().lock();
try{
Root r = getRoot();
if (index < 0 || index > r.size)
throw new IndexOutOfBoundsException();
Iter iter = new Iter();
iter.next = r.first;
//scroll to requested position
//TODO scroll from end, if beyond half
for (int i = 0; i < index; i++) {
iter.next();
}
return iter;
}finally {
lock.readLock().unlock();
}
}
Root getRoot(){
//expect that caller already holds lock
try {
return db.fetch(rootRecid,ROOT_SERIALIZER);
} catch (IOException e) {
throw new IOError(e);
}
}
public int size() {
lock.readLock().lock();
try{
return (int) getRoot().size;
}finally {
lock.readLock().unlock();
}
}
public Iterator<E> descendingIterator() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
public boolean add(Object value) {
lock.writeLock().lock();
try {
Root r = getRoot();
Entry e = new Entry(r.last, 0, value);
long recid = db.insert(e, entrySerializer,false);
//update old last Entry to point to new record
if (r.last != 0) {
Entry oldLast = db.fetch(r.last, entrySerializer);
if (oldLast.next != 0) throw new Error();
oldLast.next = recid;
db.update(r.last, oldLast, entrySerializer);
}
//update linked list
r.last = recid;
if (r.first == 0) r.first = recid;
r.size++;
db.update(rootRecid, r, ROOT_SERIALIZER);
modCount++;
return true;
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.writeLock().unlock();
}
}
private Entry<E> fetch(long recid) {
lock.readLock().lock();
try {
return db.fetch(recid, entrySerializer);
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.readLock().unlock();
}
}
/**
* called from Serialization object
*/
static LinkedList2 deserialize(DataInput is, Serialization ser) throws IOException, ClassNotFoundException {
long rootrecid = LongPacker.unpackLong(is);
long sizeLimit = LongPacker.unpackLong(is);
if(sizeLimit!=0) throw new InternalError("LinkedList.sizeLimit not supported in this JDBM version");
Serializer serializer = (Serializer) ser.deserialize(is);
return new LinkedList2(ser.db,rootrecid, serializer);
}
void serialize(DataOutput out) throws IOException {
LongPacker.packLong(out, rootRecid);
LongPacker.packLong(out, sizeLimit);
db.defaultSerializer().serialize(out, valueSerializer);
}
private final Serializer<Entry> entrySerializer = new Serializer<Entry>() {
public void serialize(DataOutput out, Entry e) throws IOException {
LongPacker.packLong(out, e.prev);
LongPacker.packLong(out, e.next);
if (valueSerializer != null)
valueSerializer.serialize(out, (E) e.value);
else
db.defaultSerializer().serialize(out, e.value);
}
public Entry<E> deserialize(DataInput in) throws IOException, ClassNotFoundException {
long prev = LongPacker.unpackLong(in);
long next = LongPacker.unpackLong(in);
Object value = null;
if (loadValues)
value = valueSerializer == null ? db.defaultSerializer().deserialize(in) : valueSerializer.deserialize(in);
return new LinkedList2.Entry(prev, next, value);
}
};
static class Entry<E> {
long prev = 0;
long next = 0;
E value;
public Entry(long prev, long next, E value) {
this.prev = prev;
this.next = next;
this.value = value;
}
}
private final class Iter implements ListIterator<E> {
private int expectedModCount = modCount;
private int index = 0;
private long prev = 0;
private long next = 0;
private byte lastOper = 0;
public boolean hasNext() {
return next != 0;
}
public E next() {
if (next == 0) throw new NoSuchElementException();
checkForComodification();
Entry<E> e = fetch(next);
prev = next;
next = e.next;
index++;
lastOper = +1;
return e.value;
}
public boolean hasPrevious() {
return prev != 0;
}
public E previous() {
checkForComodification();
Entry<E> e = fetch(prev);
next = prev;
prev = e.prev;
index--;
lastOper = -1;
return e.value;
}
public int nextIndex() {
return index;
}
public int previousIndex() {
return index - 1;
}
public void remove() {
checkForComodification();
lock.writeLock().lock();
try {
if (lastOper == 1) {
//last operation was next() so remove previous element
lastOper = 0;
Entry<E> p = db.fetch(prev, entrySerializer);
//update entry before previous
if (p.prev != 0) {
Entry<E> pp = db.fetch(p.prev, entrySerializer);
pp.next = p.next;
db.update(p.prev, pp, entrySerializer);
}
//update entry after next
if (p.next != 0) {
Entry<E> pn = db.fetch(p.next, entrySerializer);
pn.prev = p.prev;
db.update(p.next, pn, entrySerializer);
}
//remove old record from db
db.delete(prev);
//update list
Root r = getRoot();
if (r.first == prev)
r.first = next;
if (r.last == prev)
r.last = next;
r.size--;
db.update(rootRecid, r,ROOT_SERIALIZER);
modCount++;
expectedModCount++;
//update iterator
prev = p.prev;
} else if (lastOper == -1) {
//last operation was prev() so remove next element
lastOper = 0;
Entry<E> n = db.fetch(next, entrySerializer);
//update entry before next
if (n.prev != 0) {
Entry<E> pp = db.fetch(n.prev, entrySerializer);
pp.next = n.next;
db.update(n.prev, pp, entrySerializer);
}
//update entry after previous
if (n.next != 0) {
Entry<E> pn = db.fetch(n.next, entrySerializer);
pn.prev = n.prev;
db.update(n.next, pn, entrySerializer);
}
//remove old record from db
db.delete(next);
//update list
Root r = getRoot();
if (r.last == next)
r.last = prev;
if (r.first == next)
r.first = prev;
r.size--;
db.update(rootRecid, r,ROOT_SERIALIZER);
modCount++;
expectedModCount++;
//update iterator
next = n.next;
} else
throw new IllegalStateException();
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.writeLock().unlock();
}
}
public void set(E value) {
checkForComodification();
lock.writeLock().lock();
try {
if (lastOper == 1) {
//last operation was next(), so update previous item
lastOper = 0;
Entry<E> n = db.fetch(prev, entrySerializer);
n.value = value;
db.update(prev, n, entrySerializer);
} else if (lastOper == -1) {
//last operation was prev() so update next item
lastOper = 0;
Entry<E> n = db.fetch(next, entrySerializer);
n.value = value;
db.update(next, n, entrySerializer);
} else
throw new IllegalStateException();
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.writeLock().unlock();
}
}
public void add(E value) {
checkForComodification();
//use more efficient method if possible
if (next == 0) {
LinkedList2.this.add(value);
expectedModCount++;
return;
}
lock.writeLock().lock();
try {
//insert new entry
Entry<E> e = new Entry<E>(prev, next, value);
long recid = db.insert(e, entrySerializer,false);
//update previous entry
if (prev != 0) {
Entry<E> p = db.fetch(prev, entrySerializer);
if (p.next != next) throw new Error();
p.next = recid;
db.update(prev, p, entrySerializer);
}
//update next entry
Entry<E> n = fetch(next);
if (n.prev != prev) throw new Error();
n.prev = recid;
db.update(next, n, entrySerializer);
//update List
Root r = getRoot();
r.size++;
db.update(rootRecid, r, ROOT_SERIALIZER);
//update iterator
expectedModCount++;
modCount++;
prev = recid;
} catch (IOException e) {
throw new IOError(e);
}finally {
lock.writeLock().unlock();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
/**
* Copyes collection from one db to other, while keeping logical recids unchanged
*/
static void defrag(long recid, DBStore r1, DBStore r2) throws IOException {
try {
//move linked list itself
byte[] data = r1.fetchRaw(recid);
r2.forceInsert(recid, data);
DataInputOutput in = new DataInputOutput();
in.reset(data);
LinkedList2 l = (LinkedList2) r1.defaultSerializer().deserialize(in);
l.loadValues = false;
//move linkedlist root
if(l.rootRecid == 0) //empty list, done
return;
data = r1.fetchRaw(l.rootRecid);
r2.forceInsert(l.rootRecid, data);
in.reset(data);
Root r = ROOT_SERIALIZER.deserialize(in);
//move all other nodes in linked list
long current = r.first;
while (current != 0) {
data = r1.fetchRaw(current);
in.reset(data);
r2.forceInsert(current, data);
Entry e = (Entry) l.entrySerializer.deserialize(in);
current = e.next;
}
} catch (ClassNotFoundException e) {
throw new IOError(e);
}
}
}

View File

@ -0,0 +1,239 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.IOException;
import java.util.Arrays;
/**
* This class manages the linked lists of logical rowid pages.
*/
final class LogicalRowIdManager {
// our record file and associated page manager
private final PageFile file;
private final PageManager pageman;
static final short ELEMS_PER_PAGE = (short) ((Storage.PAGE_SIZE - Magic.PAGE_HEADER_SIZE) / Magic.PhysicalRowId_SIZE);
private long[] freeRecordsInTransRowid = new long[4];
private int freeRecordsInTransSize = 0;
/** number of free logical rowids on logical free page, is SHORT*/
static final int OFFSET_FREE_COUNT = Magic.PAGE_HEADER_SIZE;
static final int FREE_HEADER_SIZE = Magic.PAGE_HEADER_SIZE + Magic.SZ_SHORT;
/** maximal number of free logical per page */
static final int FREE_RECORDS_PER_PAGE = (Storage.PAGE_SIZE -FREE_HEADER_SIZE)/6;
/**
* Creates a log rowid manager using the indicated record file and page manager
*/
LogicalRowIdManager(PageFile file, PageManager pageman) throws IOException {
this.file = file;
this.pageman = pageman;
}
/**
* Creates a new logical rowid pointing to the indicated physical id
*
* @param physloc physical location to point to
* @return logical recid
*/
long insert(final long physloc) throws IOException {
// check whether there's a free rowid to reuse
long retval = getFreeSlot();
if (retval == 0) {
// no. This means that we bootstrap things by allocating
// a new translation page and freeing all the rowids on it.
long firstPage = pageman.allocate(Magic.TRANSLATION_PAGE);
short curOffset = Magic.PAGE_HEADER_SIZE;
for (int i = 0; i < ELEMS_PER_PAGE; i++) {
putFreeSlot(((-firstPage) << Storage.PAGE_SIZE_SHIFT) + (long) curOffset);
curOffset += Magic.PhysicalRowId_SIZE;
}
retval = getFreeSlot();
if (retval == 0) {
throw new Error("couldn't obtain free translation");
}
}
// write the translation.
update(retval, physloc);
return retval;
}
/**
* Insert at forced location, use only for defragmentation !!
*
* @param logicalRowId
* @param physLoc
* @throws IOException
*/
void forceInsert(final long logicalRowId, final long physLoc) throws IOException {
if (fetch(logicalRowId) != 0)
throw new Error("can not forceInsert, record already exists: " + logicalRowId);
update(logicalRowId, physLoc);
}
/**
* Releases the indicated logical rowid.
*/
void delete(final long logicalrowid) throws IOException {
//zero out old location, is needed for defragmentation
final long pageId = -(logicalrowid>>> Storage.PAGE_SIZE_SHIFT);
final PageIo xlatPage = file.get(pageId);
xlatPage.pageHeaderSetLocation((short) (logicalrowid & Storage.OFFSET_MASK), 0);
file.release(pageId, true);
putFreeSlot(logicalrowid);
}
/**
* Updates the mapping
*
* @param logicalrowid The logical rowid
* @param physloc The physical rowid
*/
void update(final long logicalrowid, final long physloc) throws IOException {
final long pageId = -(logicalrowid>>> Storage.PAGE_SIZE_SHIFT);
final PageIo xlatPage = file.get(pageId);
xlatPage.pageHeaderSetLocation((short) (logicalrowid & Storage.OFFSET_MASK), physloc);
file.release(pageId, true);
}
/**
* Returns a mapping
*
* @param logicalrowid The logical rowid
* @return The physical rowid, 0 if does not exist
*/
long fetch(long logicalrowid) throws IOException {
final long pageId = -(logicalrowid>>> Storage.PAGE_SIZE_SHIFT);
final long last = pageman.getLast(Magic.TRANSLATION_PAGE);
if (last - 1 > pageId)
return 0;
final short offset = (short) (logicalrowid & Storage.OFFSET_MASK);
final PageIo xlatPage = file.get(pageId);
final long ret = xlatPage.pageHeaderGetLocation(offset);
file.release(pageId, false);
return ret;
}
void commit() throws IOException {
if(freeRecordsInTransSize==0) return;
long freeRecPageId = pageman.getLast(Magic.FREELOGIDS_PAGE);
if(freeRecPageId == 0){
//allocate new
freeRecPageId = pageman.allocate(Magic.FREELOGIDS_PAGE);
}
PageIo freeRecPage = file.get(freeRecPageId);
//write all uncommited free records
for(int rowPos = 0;rowPos<freeRecordsInTransSize;rowPos++){
short count = freeRecPage.readShort(OFFSET_FREE_COUNT);
if(count == FREE_RECORDS_PER_PAGE){
//allocate new free recid page
file.release(freeRecPage);
freeRecPageId = pageman.allocate(Magic.FREELOGIDS_PAGE);
freeRecPage = file.get(freeRecPageId);
freeRecPage.writeShort(FREE_RECORDS_PER_PAGE, (short)0);
count = 0;
}
final int offset = (count ) *6 + FREE_HEADER_SIZE;
//write free recid and increase counter
freeRecPage.writeSixByteLong(offset,freeRecordsInTransRowid[rowPos]);
count++;
freeRecPage.writeShort(OFFSET_FREE_COUNT, count);
}
file.release(freeRecPage);
clearFreeRecidsInTransaction();
}
private void clearFreeRecidsInTransaction() {
if(freeRecordsInTransRowid.length>128)
freeRecordsInTransRowid = new long[4];
freeRecordsInTransSize = 0;
}
void rollback() throws IOException {
clearFreeRecidsInTransaction();
}
/**
* Returns a free Logical rowid, or
* 0 if nothing was found.
*/
long getFreeSlot() throws IOException {
if (freeRecordsInTransSize != 0) {
return freeRecordsInTransRowid[--freeRecordsInTransSize];
}
final long logicFreePageId = pageman.getLast(Magic.FREELOGIDS_PAGE);
if(logicFreePageId == 0) {
return 0;
}
PageIo logicFreePage = file.get(logicFreePageId);
short recCount = logicFreePage.readShort(OFFSET_FREE_COUNT);
if(recCount <= 0){
throw new InternalError();
}
final int offset = (recCount -1) *6 + FREE_HEADER_SIZE;
final long ret = logicFreePage.readSixByteLong(offset);
recCount--;
if(recCount>0){
//decrease counter and zero out old record
logicFreePage.writeSixByteLong(offset,0);
logicFreePage.writeShort(OFFSET_FREE_COUNT, recCount);
file.release(logicFreePage);
}else{
//release this page
file.release(logicFreePage);
pageman.free(Magic.FREELOGIDS_PAGE,logicFreePageId);
}
return ret;
}
/**
* Puts the indicated rowid on the free list
*/
void putFreeSlot(long rowid) throws IOException {
//ensure capacity
if(freeRecordsInTransSize == freeRecordsInTransRowid.length)
freeRecordsInTransRowid = Arrays.copyOf(freeRecordsInTransRowid, freeRecordsInTransRowid.length * 4);
//add record and increase size
freeRecordsInTransRowid[freeRecordsInTransSize]=rowid;
freeRecordsInTransSize++;
}
}

View File

@ -0,0 +1,432 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jdbm;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* Hash Map which uses primitive long as key.
* Main advantage is new instanceof of Long does not have to be created for each lookup.
* <p/>
* This code comes from Android, which in turns comes from Apache Harmony.
* This class was modified to use primitive longs and stripped down to consume less space.
* <p/>
* Author of JDBM modifications: Jan Kotek
*/
class LongHashMap<V> implements Serializable {
private static final long serialVersionUID = 362499999763181265L;
private int elementCount;
private Entry<V>[] elementData;
private final float loadFactor;
private int threshold;
private int defaultSize = 16;
private transient Entry<V> reuseAfterDelete = null;
static final class Entry<V> implements Serializable{
private static final long serialVersionUID = 362445231113181265L;
Entry<V> next;
V value;
long key;
Entry(long theKey) {
this.key = theKey;
this.value = null;
}
}
static class HashMapIterator<V> implements Iterator<V> {
private int position = 0;
boolean canRemove = false;
Entry<V> entry;
Entry<V> lastEntry;
final LongHashMap<V> associatedMap;
HashMapIterator(LongHashMap<V> hm) {
associatedMap = hm;
}
public boolean hasNext() {
if (entry != null) {
return true;
}
Entry<V>[] elementData = associatedMap.elementData;
int length = elementData.length;
int newPosition = position;
boolean result = false;
while (newPosition < length) {
if (elementData[newPosition] == null) {
newPosition++;
} else {
result = true;
break;
}
}
position = newPosition;
return result;
}
public V next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Entry<V> result;
Entry<V> _entry = entry;
if (_entry == null) {
result = lastEntry = associatedMap.elementData[position++];
entry = lastEntry.next;
} else {
if (lastEntry.next != _entry) {
lastEntry = lastEntry.next;
}
result = _entry;
entry = _entry.next;
}
canRemove = true;
return result.value;
}
public void remove() {
if (!canRemove) {
throw new IllegalStateException();
}
canRemove = false;
if (lastEntry.next == entry) {
while (associatedMap.elementData[--position] == null) {
// Do nothing
}
associatedMap.elementData[position] = associatedMap.elementData[position].next;
entry = null;
} else {
lastEntry.next = entry;
}
if (lastEntry != null) {
Entry<V> reuse = lastEntry;
lastEntry = null;
reuse.key = Long.MIN_VALUE;
reuse.value = null;
associatedMap.reuseAfterDelete = reuse;
}
associatedMap.elementCount--;
}
}
@SuppressWarnings("unchecked")
private Entry<V>[] newElementArray(int s) {
return new Entry[s];
}
/**
* Constructs a new empty {@code HashMap} instance.
*
* @since Android 1.0
*/
public LongHashMap() {
this(16);
}
/**
* Constructs a new {@code HashMap} instance with the specified capacity.
*
* @param capacity the initial capacity of this hash map.
* @throws IllegalArgumentException when the capacity is less than zero.
* @since Android 1.0
*/
public LongHashMap(int capacity) {
defaultSize = capacity;
if (capacity >= 0) {
elementCount = 0;
elementData = newElementArray(capacity == 0 ? 1 : capacity);
loadFactor = 0.75f; // Default load factor of 0.75
computeMaxSize();
} else {
throw new IllegalArgumentException();
}
}
// BEGIN android-changed
/**
* Removes all mappings from this hash map, leaving it empty.
*
* @see #isEmpty
* @see #size
* @since Android 1.0
*/
public void clear() {
if (elementCount > 0) {
elementCount = 0;
}
if(elementData.length>1024 && elementData.length>defaultSize)
elementData = new Entry[defaultSize];
else
Arrays.fill(elementData, null);
computeMaxSize();
}
// END android-changed
/**
* Returns a shallow copy of this map.
*
* @return a shallow copy of this map.
* @since Android 1.0
*/
private void computeMaxSize() {
threshold = (int) (elementData.length * loadFactor);
}
/**
* Returns the value of the mapping with the specified key.
*
* @param key the key.
* @return the value of the mapping with the specified key, or {@code null}
* if no mapping for the specified key is found.
* @since Android 1.0
*/
public V get(final long key) {
final int hash = powerHash(key);
final int index = (hash & 0x7FFFFFFF) % elementData.length;
//find non null entry
Entry<V> m = elementData[index];
while (m != null) {
if (key == m.key)
return m.value;
m = m.next;
}
return null;
}
/**
* Returns whether this map is empty.
*
* @return {@code true} if this map has no elements, {@code false}
* otherwise.
* @see #size()
* @since Android 1.0
*/
public boolean isEmpty() {
return elementCount == 0;
}
/**
* @return iterator over keys
*/
// public Iterator<K> keyIterator(){
// return new HashMapIterator<K, K, V>(
// new MapEntry.Type<K, K, V>() {
// public K get(Entry<K, V> entry) {
// return entry.key;
// }
// }, HashMap.this);
//
// }
/**
* Maps the specified key to the specified value.
*
* @param key the key.
* @param value the value.
* @return the value of any previous mapping with the specified key or
* {@code null} if there was no such mapping.
* @since Android 1.0
*/
public V put(final long key, final V value) {
int hash = powerHash(key);
int index = (hash & 0x7FFFFFFF) % elementData.length;
//find non null entry
Entry<V> entry = elementData[index];
while (entry != null && key != entry.key) {
entry = entry.next;
}
if (entry == null) {
if (++elementCount > threshold) {
rehash();
index = (hash & 0x7FFFFFFF) % elementData.length;
}
entry = createHashedEntry(key, index);
}
V result = entry.value;
entry.value = value;
return result;
}
Entry<V> createHashedEntry(final long key, final int index) {
Entry<V> entry = reuseAfterDelete;
if (entry == null) {
entry = new Entry<V>(key);
} else {
reuseAfterDelete = null;
entry.key = key;
entry.value = null;
}
entry.next = elementData[index];
elementData[index] = entry;
return entry;
}
void rehash(final int capacity) {
int length = (capacity == 0 ? 1 : capacity << 1);
Entry<V>[] newData = newElementArray(length);
for (int i = 0; i < elementData.length; i++) {
Entry<V> entry = elementData[i];
while (entry != null) {
int index = ((int) powerHash(entry.key) & 0x7FFFFFFF) % length;
Entry<V> next = entry.next;
entry.next = newData[index];
newData[index] = entry;
entry = next;
}
}
elementData = newData;
computeMaxSize();
}
void rehash() {
rehash(elementData.length);
}
/**
* Removes the mapping with the specified key from this map.
*
* @param key the key of the mapping to remove.
* @return the value of the removed mapping or {@code null} if no mapping
* for the specified key was found.
* @since Android 1.0
*/
public V remove(final long key) {
Entry<V> entry = removeEntry(key);
if (entry == null)
return null;
V ret = entry.value;
entry.value = null;
entry.key = Long.MIN_VALUE;
reuseAfterDelete = entry;
return ret;
}
Entry<V> removeEntry(final long key) {
Entry<V> last = null;
final int hash = powerHash(key);
final int index = (hash & 0x7FFFFFFF) % elementData.length;
Entry<V> entry = elementData[index];
while (true) {
if (entry == null) {
return null;
}
if (key == entry.key) {
if (last == null) {
elementData[index] = entry.next;
} else {
last.next = entry.next;
}
elementCount--;
return entry;
}
last = entry;
entry = entry.next;
}
}
/**
* Returns the number of elements in this map.
*
* @return the number of elements in this map.
* @since Android 1.0
*/
public int size() {
return elementCount;
}
/**
* @returns iterator over values in map
*/
public Iterator<V> valuesIterator() {
return new HashMapIterator<V>(this);
}
static final private int powerHash(final long key){
int h = (int)(key ^ (key >>> 32));
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
}

View File

@ -0,0 +1,106 @@
/*
Copyright (c) 2008, Nathan Sweet
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.apache.jdbm;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* Packing utility for non-negative <code>long</code> and values.
* <p/>
* Originally developed for Kryo by Nathan Sweet.
* Modified for JDBM by Jan Kotek
*/
public final class LongPacker {
/**
* Pack non-negative long into output stream.
* It will occupy 1-10 bytes depending on value (lower values occupy smaller space)
*
* @param os
* @param value
* @throws IOException
*/
static public void packLong(DataOutput os, long value) throws IOException {
if (value < 0) {
throw new IllegalArgumentException("negative value: v=" + value);
}
while ((value & ~0x7FL) != 0) {
os.write((((int) value & 0x7F) | 0x80));
value >>>= 7;
}
os.write((byte) value);
}
/**
* Unpack positive long value from the input stream.
*
* @param is The input stream.
* @return The long value.
* @throws java.io.IOException
*/
static public long unpackLong(DataInput is) throws IOException {
long result = 0;
for (int offset = 0; offset < 64; offset += 7) {
long b = is.readUnsignedByte();
result |= (b & 0x7F) << offset;
if ((b & 0x80) == 0) {
return result;
}
}
throw new Error("Malformed long.");
}
/**
* Pack non-negative long into output stream.
* It will occupy 1-5 bytes depending on value (lower values occupy smaller space)
*
* @param os
* @param value
* @throws IOException
*/
static public void packInt(DataOutput os, int value) throws IOException {
if (value < 0) {
throw new IllegalArgumentException("negative value: v=" + value);
}
while ((value & ~0x7F) != 0) {
os.write(((value & 0x7F) | 0x80));
value >>>= 7;
}
os.write((byte) value);
}
static public int unpackInt(DataInput is) throws IOException {
for (int offset = 0, result = 0; offset < 32; offset += 7) {
int b = is.readUnsignedByte();
result |= (b & 0x7F) << offset;
if ((b & 0x80) == 0) {
return result;
}
}
throw new Error("Malformed integer.");
}
}

View File

@ -0,0 +1,105 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
/**
* This interface contains magic cookies.
*/
interface Magic {
/**
* Magic cookie at start of file
*/
short FILE_HEADER = 0x1350;
/**
* Magic for pages. They're offset by the page type magic codes.
*/
short PAGE_MAGIC = 0x1351;
/**
* Magics for pages in certain lists.
*/
short FREE_PAGE = 0;
short USED_PAGE = 1;
short TRANSLATION_PAGE = 2;
short FREELOGIDS_PAGE = 3;
short FREEPHYSIDS_PAGE = 4;
short FREEPHYSIDS_ROOT_PAGE = 5;
/**
* Number of lists in a file
*/
short NLISTS = 6;
/**
* Magic for transaction file
*/
short LOGFILE_HEADER = 0x1360;
/**
* Size of an externalized byte
*/
short SZ_BYTE = 1;
/**
* Size of an externalized short
*/
short SZ_SHORT = 2;
/**
* Size of an externalized int
*/
short SZ_INT = 4;
/**
* Size of an externalized long
*/
short SZ_LONG = 8;
/**
* size of three byte integer
*/
short SZ_SIX_BYTE_LONG = 6;
/**offsets in file header (zero page in file)*/
short FILE_HEADER_O_MAGIC = 0; // short magic
short FILE_HEADER_O_LISTS = Magic.SZ_SHORT; // long[2*NLISTS]
int FILE_HEADER_O_ROOTS = FILE_HEADER_O_LISTS + (Magic.NLISTS * 2 * Magic.SZ_LONG);
/**
* The number of "root" rowids available in the file.
*/
int FILE_HEADER_NROOTS = 16;
short PAGE_HEADER_O_MAGIC = 0; // short magic
short PAGE_HEADER_O_NEXT = Magic.SZ_SHORT;
short PAGE_HEADER_O_PREV = PAGE_HEADER_O_NEXT + Magic.SZ_SIX_BYTE_LONG;
short PAGE_HEADER_SIZE = PAGE_HEADER_O_PREV + Magic.SZ_SIX_BYTE_LONG;
short PhysicalRowId_O_LOCATION = 0; // long page
// short PhysicalRowId_O_OFFSET = Magic.SZ_SIX_BYTE_LONG; // short offset
int PhysicalRowId_SIZE = Magic.SZ_SIX_BYTE_LONG;
short DATA_PAGE_O_FIRST = PAGE_HEADER_SIZE; // short firstrowid
short DATA_PAGE_O_DATA = (short) (DATA_PAGE_O_FIRST + Magic.SZ_SHORT);
short DATA_PER_PAGE = (short) (Storage.PAGE_SIZE - DATA_PAGE_O_DATA);
}

View File

@ -0,0 +1,26 @@
package org.apache.jdbm;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.util.ArrayList;
/**
* An alternative to <code>java.io.ObjectInputStream</code> which uses more efficient serialization
*/
public class ObjectInputStream2 extends DataInputStream implements ObjectInput {
public ObjectInputStream2(InputStream in) {
super(in);
}
public Object readObject() throws ClassNotFoundException, IOException {
//first read class data
ArrayList<SerialClassInfo.ClassInfo> info = SerialClassInfo.serializer.deserialize(this);
Serialization ser = new Serialization(null,0,info);
return ser.deserialize(this);
}
}

View File

@ -0,0 +1,25 @@
package org.apache.jdbm;
import java.io.*;
import java.util.ArrayList;
/**
* An alternative to <code>java.io.ObjectOutputStream</code> which uses more efficient serialization
*/
public class ObjectOutputStream2 extends DataOutputStream implements ObjectOutput {
public ObjectOutputStream2(OutputStream out) {
super(out);
}
public void writeObject(Object obj) throws IOException {
ArrayList registered = new ArrayList();
Serialization ser = new Serialization(null,0,registered);
byte[] data = ser.serialize(obj);
//write class info first
SerialClassInfo.serializer.serialize(this, registered);
//and write data
write(data);
}
}

View File

@ -0,0 +1,390 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import javax.crypto.Cipher;
import java.io.IOError;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Iterator;
/**
* This class represents a random access file as a set of fixed size
* records. Each record has a physical record number, and records are
* cached in order to improve access.
* <p/>
* The set of dirty records on the in-use list constitutes a transaction.
* Later on, we will send these records to some recovery thingy.
* <p/>
* PageFile is splited between more files, each with max size 1GB.
*/
final class PageFile {
final PageTransactionManager txnMgr;
/**
* Pages currently locked for read/update ops. When released the page goes
* to the dirty or clean list, depending on a flag. The file header page is
* normally locked plus the page that is currently being read or modified.
*
* @see PageIo#isDirty()
*/
private final LongHashMap<PageIo> inUse = new LongHashMap<PageIo>();
/**
* Pages whose state is dirty.
*/
private final LongHashMap<PageIo> dirty = new LongHashMap<PageIo>();
/**
* Pages in a <em>historical</em> transaction(s) that have been written
* onto the log but which have not yet been committed to the database.
*/
private final LongHashMap<PageIo> inTxn = new LongHashMap<PageIo>();
// transactions disabled?
final boolean transactionsDisabled;
/**
* A array of clean data to wipe clean pages.
*/
static final byte[] CLEAN_DATA = new byte[Storage.PAGE_SIZE];
final Storage storage;
private Cipher cipherOut;
private Cipher cipherIn;
/**
* Creates a new object on the indicated filename. The file is
* opened in read/write mode.
*
* @param fileName the name of the file to open or create, without
* an extension.
* @throws IOException whenever the creation of the underlying
* RandomAccessFile throws it.
*/
PageFile(String fileName, boolean readonly, boolean transactionsDisabled, Cipher cipherIn, Cipher cipherOut, boolean useRandomAccessFile, boolean lockingDisabled) throws IOException {
this.cipherIn = cipherIn;
this.cipherOut = cipherOut;
this.transactionsDisabled = transactionsDisabled;
if(fileName == null){
this.storage = new StorageMemory(transactionsDisabled);
}else if(DBMaker.isZipFileLocation(fileName)!=null)
this.storage = new StorageZip(DBMaker.isZipFileLocation(fileName));
// }else if (fileName.contains("!/"))
// this.storage = new StorageZip(fileName);
else if(useRandomAccessFile)
this.storage = new StorageDisk(fileName,readonly,lockingDisabled);
else
this.storage = new StorageDiskMapped(fileName,readonly,transactionsDisabled,lockingDisabled);
if (this.storage.isReadonly() && !readonly)
throw new IllegalArgumentException("This type of storage is readonly, you should call readonly() on DBMaker");
if (!readonly && !transactionsDisabled) {
txnMgr = new PageTransactionManager(this, storage, cipherIn, cipherOut);
} else {
txnMgr = null;
}
}
public PageFile(String filename) throws IOException {
this(filename, false, false, null, null,false,false);
}
/**
* Gets a page from the file. The returned byte array is
* the in-memory copy of the record, and thus can be written
* (and subsequently released with a dirty flag in order to
* write the page back). If transactions are disabled, changes
* may be written directly
*
* @param pageId The record number to retrieve.
*/
PageIo get(long pageId) throws IOException {
// try in transaction list, dirty list, free list
PageIo node = inTxn.get(pageId);
if (node != null) {
inTxn.remove(pageId);
inUse.put(pageId, node);
return node;
}
node = dirty.get(pageId);
if (node != null) {
dirty.remove(pageId);
inUse.put(pageId, node);
return node;
}
// sanity check: can't be on in use list
if (inUse.get(pageId) != null) {
throw new Error("double get for page " + pageId);
}
//read node from file
if (cipherOut == null) {
node = new PageIo(pageId,storage.read(pageId));
} else {
//decrypt if needed
ByteBuffer b = storage.read(pageId);
byte[] bb;
if(b.hasArray()){
bb = b.array();
}else{
bb = new byte[Storage.PAGE_SIZE];
b.position(0);
b.get(bb, 0, Storage.PAGE_SIZE);
}
if (!Utils.allZeros(bb)) try {
bb = cipherOut.doFinal(bb);
node = new PageIo(pageId, ByteBuffer.wrap(bb));
} catch (Exception e) {
throw new IOError(e);
}else {
node = new PageIo(pageId, ByteBuffer.wrap(PageFile.CLEAN_DATA).asReadOnlyBuffer());
}
}
inUse.put(pageId, node);
node.setClean();
return node;
}
/**
* Releases a page.
*
* @param pageId The record number to release.
* @param isDirty If true, the page was modified since the get().
*/
void release(final long pageId, final boolean isDirty) throws IOException {
final PageIo page = inUse.remove(pageId);
if (!page.isDirty() && isDirty)
page.setDirty();
if (page.isDirty()) {
dirty.put(pageId, page);
} else if (!transactionsDisabled && page.isInTransaction()) {
inTxn.put(pageId, page);
}
}
/**
* Releases a page.
*
* @param page The page to release.
*/
void release(final PageIo page) throws IOException {
final long key = page.getPageId();
inUse.remove(key);
if (page.isDirty()) {
// System.out.println( "Dirty: " + key + page );
dirty.put(key, page);
} else if (!transactionsDisabled && page.isInTransaction()) {
inTxn.put(key, page);
}
}
/**
* Discards a page (will not write the page even if it's dirty)
*
* @param page The page to discard.
*/
void discard(PageIo page) {
long key = page.getPageId();
inUse.remove(key);
}
/**
* Commits the current transaction by flushing all dirty buffers
* to disk.
*/
void commit() throws IOException {
// debugging...
if (!inUse.isEmpty() && inUse.size() > 1) {
showList(inUse.valuesIterator());
throw new Error("in use list not empty at commit time ("
+ inUse.size() + ")");
}
// System.out.println("committing...");
if (dirty.size() == 0) {
// if no dirty pages, skip commit process
return;
}
if (!transactionsDisabled) {
txnMgr.start();
}
//sort pages by IDs
long[] pageIds = new long[dirty.size()];
int c = 0;
for (Iterator<PageIo> i = dirty.valuesIterator(); i.hasNext(); ) {
pageIds[c] = i.next().getPageId();
c++;
}
Arrays.sort(pageIds);
for (long pageId : pageIds) {
PageIo node = dirty.get(pageId);
// System.out.println("node " + node + " map size now " + dirty.size());
if (transactionsDisabled) {
if(cipherIn !=null)
storage.write(node.getPageId(), ByteBuffer.wrap(Utils.encrypt(cipherIn, node.getData())));
else
storage.write(node.getPageId(),node.getData());
node.setClean();
} else {
txnMgr.add(node);
inTxn.put(node.getPageId(), node);
}
}
dirty.clear();
if (!transactionsDisabled) {
txnMgr.commit();
}
}
/**
* Rollback the current transaction by discarding all dirty buffers
*/
void rollback() throws IOException {
// debugging...
if (!inUse.isEmpty()) {
showList(inUse.valuesIterator());
throw new Error("in use list not empty at rollback time ("
+ inUse.size() + ")");
}
// System.out.println("rollback...");
dirty.clear();
txnMgr.synchronizeLogFromDisk();
if (!inTxn.isEmpty()) {
showList(inTxn.valuesIterator());
throw new Error("in txn list not empty at rollback time ("
+ inTxn.size() + ")");
}
;
}
/**
* Commits and closes file.
*/
void close() throws IOException {
if (!dirty.isEmpty()) {
commit();
}
if(!transactionsDisabled && txnMgr!=null){
txnMgr.shutdown();
}
if (!inTxn.isEmpty()) {
showList(inTxn.valuesIterator());
throw new Error("In transaction not empty");
}
// these actually ain't that bad in a production release
if (!dirty.isEmpty()) {
System.out.println("ERROR: dirty pages at close time");
showList(dirty.valuesIterator());
throw new Error("Dirty pages at close time");
}
if (!inUse.isEmpty()) {
System.out.println("ERROR: inUse pages at close time");
showList(inUse.valuesIterator());
throw new Error("inUse pages at close time");
}
storage.sync();
storage.forceClose();
}
/**
* Force closing the file and underlying transaction manager.
* Used for testing purposed only.
*/
void forceClose() throws IOException {
if(!transactionsDisabled){
txnMgr.forceClose();
}
storage.forceClose();
}
/**
* Prints contents of a list
*/
private void showList(Iterator<PageIo> i) {
int cnt = 0;
while (i.hasNext()) {
System.out.println("elem " + cnt + ": " + i.next());
cnt++;
}
}
/**
* Synchs a node to disk. This is called by the transaction manager's
* synchronization code.
*/
void synch(PageIo node) throws IOException {
ByteBuffer data = node.getData();
if (data != null) {
if(cipherIn!=null)
storage.write(node.getPageId(), ByteBuffer.wrap(Utils.encrypt(cipherIn, data)));
else
storage.write(node.getPageId(), data);
}
}
/**
* Releases a node from the transaction list, if it was sitting
* there.
*/
void releaseFromTransaction(PageIo node)
throws IOException {
inTxn.remove(node.getPageId());
}
/**
* Synchronizes the file.
*/
void sync() throws IOException {
storage.sync();
}
public int getDirtyPageCount() {
return dirty.size();
}
public void deleteAllFiles() throws IOException {
storage.deleteAllFiles();
}
}

View File

@ -0,0 +1,448 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import javax.crypto.Cipher;
import java.io.*;
import java.nio.ByteBuffer;
import static org.apache.jdbm.Magic.*;
/**
* Wraps a page sizes ByteBuffer for reading and writing.
* <p>
* ByteBuffer may be subview of a larger buffer (ie large buffer mapped over a file).
* In this case ByteBuffer will have set limit, mark and other variables to limit its size.
* <p>
* For reading buffered may be shared. For example StoreMemory just returns its pages without copying.
* In this case buffer is marked as 'readonly' and needs to be copied before write (Copy On Write - COW).
* COW is not necessary if transactions are disabled and changes can not be rolled back.
* <p>
*/
final class PageIo {
private long pageId;
private ByteBuffer data; // work area
/** buffers contains changes which were not written to disk yet. */
private boolean dirty = false;
private int transactionCount = 0;
/**
* Default constructor for serialization
*/
public PageIo() {
// empty
}
/**
* Constructs a new PageIo instance working on the indicated
* buffer.
*/
PageIo(long pageId, byte[] data) {
this.pageId = pageId;
this.data = ByteBuffer.wrap(data);
}
public PageIo(long pageId, ByteBuffer data) {
this.pageId = pageId;
this.data = data;
}
/** Frequent reads on direct buffer may be slower then on heap buffer.
* This method converts native direct to heap buffer
*/
void ensureHeapBuffer(){
if(data.isDirect()){
final byte[] bb = new byte[Storage.PAGE_SIZE];
data.get(bb,0,Storage.PAGE_SIZE);
data = ByteBuffer.wrap(bb);
if(data.isReadOnly()) throw new InternalError();
}
}
/**
* Returns the underlying array
*/
ByteBuffer getData() {
return data;
}
/**
* Returns the page number.
*/
long getPageId() {
return pageId;
}
/**
* Sets the dirty flag
*/
void setDirty() {
dirty = true;
if(data.isReadOnly()){
// make copy if needed, so we can write into buffer
byte[] buf = new byte[Storage.PAGE_SIZE];
data.get(buf,0,Storage.PAGE_SIZE);
data = ByteBuffer.wrap(buf);
}
}
/**
* Clears the dirty flag
*/
void setClean() {
dirty = false;
}
/**
* Returns true if the dirty flag is set.
*/
boolean isDirty() {
return dirty;
}
/**
* Returns true if the block is still dirty with respect to the
* transaction log.
*/
boolean isInTransaction() {
return transactionCount != 0;
}
/**
* Increments transaction count for this block, to signal that this
* block is in the log but not yet in the data file. The method also
* takes a snapshot so that the data may be modified in new transactions.
*/
void incrementTransactionCount() {
transactionCount++;
}
/**
* Decrements transaction count for this block, to signal that this
* block has been written from the log to the data file.
*/
void decrementTransactionCount() {
transactionCount--;
if (transactionCount < 0)
throw new Error("transaction count on page "
+ getPageId() + " below zero!");
}
/**
* Reads a byte from the indicated position
*/
public byte readByte(int pos) {
return data.get(pos);
}
/**
* Writes a byte to the indicated position
*/
public void writeByte(int pos, byte value) {
setDirty();
data.put(pos,value);
}
/**
* Reads a short from the indicated position
*/
public short readShort(int pos) {
return data.getShort(pos);
}
/**
* Writes a short to the indicated position
*/
public void writeShort(int pos, short value) {
setDirty();
data.putShort(pos,value);
}
/**
* Reads an int from the indicated position
*/
public int readInt(int pos) {
return data.getInt(pos);
}
/**
* Writes an int to the indicated position
*/
public void writeInt(int pos, int value) {
setDirty();
data.putInt(pos,value);
}
/**
* Reads a long from the indicated position
*/
public long readLong(int pos) {
return data.getLong(pos);
}
/**
* Writes a long to the indicated position
*/
public void writeLong(int pos, long value) {
setDirty();
data.putLong(pos,value);
}
/**
* Reads a long from the indicated position
*/
public long readSixByteLong(int pos) {
long ret =
((long) (data.get(pos + 0) & 0x7f) << 40) |
((long) (data.get(pos + 1) & 0xff) << 32) |
((long) (data.get(pos + 2) & 0xff) << 24) |
((long) (data.get(pos + 3) & 0xff) << 16) |
((long) (data.get(pos + 4) & 0xff) << 8) |
((long) (data.get(pos + 5) & 0xff) << 0);
if((data.get(pos + 0) & 0x80) != 0)
return -ret;
else
return ret;
}
/**
* Writes a long to the indicated position
*/
public void writeSixByteLong(int pos, long value) {
// if(value<0) throw new IllegalArgumentException();
// if(value >> (6*8)!=0)
// throw new IllegalArgumentException("does not fit");
int negativeBit = 0;
if(value<0){
value = -value;
negativeBit = 0x80;
}
setDirty();
data.put(pos + 0,(byte) ((0x7f & (value >> 40)) | negativeBit));
data.put(pos + 1, (byte) (0xff & (value >> 32)));
data.put(pos + 2, (byte) (0xff & (value >> 24)));
data.put(pos + 3, (byte) (0xff & (value >> 16)));
data.put(pos + 4, (byte) (0xff & (value >> 8)));
data.put(pos + 5, (byte) (0xff & (value >> 0)));
}
// overrides java.lang.Object
public String toString() {
return "PageIo("
+ pageId + ","
+ dirty +")";
}
public void readExternal(DataInputStream in, Cipher cipherOut) throws IOException {
pageId = in.readLong();
byte[] data2 = new byte[Storage.PAGE_SIZE];
in.readFully(data2);
if (cipherOut == null || Utils.allZeros(data2))
data = ByteBuffer.wrap(data2);
else try {
data = ByteBuffer.wrap(cipherOut.doFinal(data2));
} catch (Exception e) {
throw new IOError(e);
}
}
public void writeExternal(DataOutput out, Cipher cipherIn) throws IOException {
out.writeLong(pageId);
out.write(Utils.encrypt(cipherIn, data.array()));
}
public byte[] getByteArray() {
if ( data.hasArray())
return data.array();
byte[] d= new byte[Storage.PAGE_SIZE];
data.rewind();
data.get(d,0,Storage.PAGE_SIZE);
return d;
}
public void writeByteArray(byte[] buf, int srcOffset, int offset, int length) {
setDirty();
data.rewind();
data.position(offset);
data.put(buf,srcOffset,length);
}
public void fileHeaderCheckHead(boolean isNew){
if (isNew)
writeShort(FILE_HEADER_O_MAGIC, Magic.FILE_HEADER);
else{
short magic = readShort(FILE_HEADER_O_MAGIC);
if(magic!=FILE_HEADER)
throw new Error("CRITICAL: file header magic not OK " + magic);
}
}
/**
* Returns the first page of the indicated list
*/
long fileHeaderGetFirstOf(int list) {
return readLong(fileHeaderOffsetOfFirst(list));
}
/**
* Sets the first page of the indicated list
*/
void fileHeaderSetFirstOf(int list, long value) {
writeLong(fileHeaderOffsetOfFirst(list), value);
}
/**
* Returns the last page of the indicated list
*/
long fileHeaderGetLastOf(int list) {
return readLong(fileHeaderOffsetOfLast(list));
}
/**
* Sets the last page of the indicated list
*/
void fileHeaderSetLastOf(int list, long value) {
writeLong(fileHeaderOffsetOfLast(list), value);
}
/**
* Returns the offset of the "first" page of the indicated list
*/
private short fileHeaderOffsetOfFirst(int list) {
return (short) (FILE_HEADER_O_LISTS + (2 * Magic.SZ_LONG * list));
}
/**
* Returns the offset of the "last" page of the indicated list
*/
private short fileHeaderOffsetOfLast(int list) {
return (short) (fileHeaderOffsetOfFirst(list) + Magic.SZ_LONG);
}
/**
* Returns the indicated root rowid. A root rowid is a special rowid
* that needs to be kept between sessions. It could conceivably be
* stored in a special file, but as a large amount of space in the
* page header is wasted anyway, it's more useful to store it where
* it belongs.
*
*/
long fileHeaderGetRoot(final int root) {
final short offset = (short) (FILE_HEADER_O_ROOTS + (root * Magic.SZ_LONG));
return readLong(offset);
}
/**
* Sets the indicated root rowid.
*
*/
void fileHeaderSetRoot(final int root, final long rowid) {
final short offset = (short) (FILE_HEADER_O_ROOTS + (root * Magic.SZ_LONG));
writeLong(offset, rowid);
}
/**
* Returns true if the magic corresponds with the fileHeader magic.
*/
boolean pageHeaderMagicOk() {
int magic = pageHeaderGetMagic();
return magic >= Magic.PAGE_MAGIC && magic <= (Magic.PAGE_MAGIC + Magic.FREEPHYSIDS_ROOT_PAGE);
}
/**
* For paranoia mode
*/
protected void pageHeaderParanoiaMagicOk() {
if (!pageHeaderMagicOk())
throw new Error("CRITICAL: page header magic not OK " + pageHeaderGetMagic());
}
short pageHeaderGetMagic() {
return readShort(PAGE_HEADER_O_MAGIC);
}
long pageHeaderGetNext() {
pageHeaderParanoiaMagicOk();
return readSixByteLong(PAGE_HEADER_O_NEXT);
}
void pageHeaderSetNext(long next) {
pageHeaderParanoiaMagicOk();
writeSixByteLong(PAGE_HEADER_O_NEXT, next);
}
long pageHeaderGetPrev() {
pageHeaderParanoiaMagicOk();
return readSixByteLong(PAGE_HEADER_O_PREV);
}
void pageHeaderSetPrev(long prev) {
pageHeaderParanoiaMagicOk();
writeSixByteLong(PAGE_HEADER_O_PREV, prev);
}
void pageHeaderSetType(short type) {
writeShort(PAGE_HEADER_O_MAGIC, (short) (Magic.PAGE_MAGIC + type));
}
long pageHeaderGetLocation(final short pos){
return readSixByteLong(pos + PhysicalRowId_O_LOCATION);
}
void pageHeaderSetLocation(short pos, long value) {
writeSixByteLong(pos + PhysicalRowId_O_LOCATION, value);
}
short dataPageGetFirst() {
return readShort(DATA_PAGE_O_FIRST);
}
void dataPageSetFirst(short value) {
pageHeaderParanoiaMagicOk();
if (value > 0 && value < DATA_PAGE_O_DATA)
throw new Error("DataPage.setFirst: offset " + value + " too small");
writeShort(DATA_PAGE_O_FIRST, value);
}
}

View File

@ -0,0 +1,247 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* This class manages the linked lists of pages that make up a file.
*/
final class PageManager {
// our record file
final PageFile file;
private PageIo headerBuf;
/**
* Creates a new page manager using the indicated record file.
*/
PageManager(PageFile file) throws IOException {
this.file = file;
// check the file headerBuf.fileHeader If the magic is 0, we assume a new
// file. Note that we hold on to the file header node.
headerBuf = file.get(0);
headerBuf.ensureHeapBuffer();
headerBuf.fileHeaderCheckHead(headerBuf.readShort(0) == 0);
}
/**
* Allocates a page of the indicated type. Returns recid of the
* page.
*/
long allocate(short type) throws IOException {
if (type == Magic.FREE_PAGE)
throw new Error("allocate of free page?");
// do we have something on the free list?
long retval = headerBuf.fileHeaderGetFirstOf(Magic.FREE_PAGE);
boolean isNew = false;
if(type!=Magic.TRANSLATION_PAGE){
if (retval != 0) {
// yes. Point to it and make the next of that page the
// new first free page.
headerBuf.fileHeaderSetFirstOf(Magic.FREE_PAGE, getNext(retval));
} else {
// nope. make a new record
retval = headerBuf.fileHeaderGetLastOf(Magic.FREE_PAGE);
if (retval == 0)
// very new file - allocate record #1
retval = 1;
headerBuf.fileHeaderSetLastOf(Magic.FREE_PAGE, retval + 1);
isNew = true;
}
}else{
//translation pages have different allocation scheme
//and also have negative address
retval = headerBuf.fileHeaderGetLastOf(Magic.TRANSLATION_PAGE) - 1;
isNew = true;
}
// Cool. We have a record, add it to the correct list
PageIo pageHdr = file.get(retval);
if(isNew){
pageHdr.pageHeaderSetType(type);
}else{
if (!pageHdr.pageHeaderMagicOk())
throw new Error("CRITICAL: page header magic for page "+
pageHdr.getPageId() + " not OK "+ pageHdr.pageHeaderGetMagic());
}
long oldLast = headerBuf.fileHeaderGetLastOf(type);
// Clean data.
pageHdr.writeByteArray(PageFile.CLEAN_DATA, 0, 0, Storage.PAGE_SIZE);
pageHdr.pageHeaderSetType(type);
pageHdr.pageHeaderSetPrev(oldLast);
pageHdr.pageHeaderSetNext(0);
if (oldLast == 0)
// This was the first one of this type
headerBuf.fileHeaderSetFirstOf(type, retval);
headerBuf.fileHeaderSetLastOf(type, retval);
file.release(retval, true);
// If there's a previous, fix up its pointer
if (oldLast != 0) {
pageHdr = file.get(oldLast);
pageHdr.pageHeaderSetNext(retval);
file.release(oldLast, true);
}
return retval;
}
/**
* Frees a page of the indicated type.
*/
void free(short type, long recid) throws IOException {
if (type == Magic.FREE_PAGE)
throw new Error("free free page?");
if (type == Magic.TRANSLATION_PAGE)
throw new Error("Translation page can not be dealocated");
if (recid == 0)
throw new Error("free header page?");
// get the page and read next and previous pointers
PageIo pageHdr = file.get(recid);
long prev = pageHdr.pageHeaderGetPrev();
long next = pageHdr.pageHeaderGetNext();
// put the page at the front of the free list.
pageHdr.pageHeaderSetType(Magic.FREE_PAGE);
pageHdr.pageHeaderSetNext(headerBuf.fileHeaderGetFirstOf(Magic.FREE_PAGE));
pageHdr.pageHeaderSetPrev(0);
headerBuf.fileHeaderSetFirstOf(Magic.FREE_PAGE, recid);
file.release(recid, true);
// remove the page from its old list
if (prev != 0) {
pageHdr = file.get(prev);
pageHdr.pageHeaderSetNext(next);
file.release(prev, true);
} else {
headerBuf.fileHeaderSetFirstOf(type, next);
}
if (next != 0) {
pageHdr = file.get(next);
pageHdr.pageHeaderSetPrev(prev);
file.release(next, true);
} else {
headerBuf.fileHeaderSetLastOf(type, prev);
}
}
/**
* Returns the page following the indicated page
*/
long getNext(long page) throws IOException {
try {
return file.get(page).pageHeaderGetNext();
} finally {
file.release(page, false);
}
}
/**
* Returns the page before the indicated page
*/
long getPrev(long page) throws IOException {
try {
return file.get(page).pageHeaderGetPrev();
} finally {
file.release(page, false);
}
}
/**
* Returns the first page on the indicated list.
*/
long getFirst(short type) throws IOException {
return headerBuf.fileHeaderGetFirstOf(type);
}
/**
* Returns the last page on the indicated list.
*/
long getLast(short type) throws IOException {
return headerBuf.fileHeaderGetLastOf(type);
}
/**
* Commit all pending (in-memory) data by flushing the page manager.
* This forces a flush of all outstanding pages (this it's an implicit
* {@link PageFile#commit} as well).
*/
void commit() throws IOException {
// write the header out
file.release(headerBuf);
file.commit();
// and obtain it again
headerBuf = file.get(0);
headerBuf.ensureHeapBuffer();
headerBuf.fileHeaderCheckHead(headerBuf.readShort(0) == 0);
}
/**
* Flushes the page manager. This forces a flush of all outstanding
* pages (this it's an implicit {@link PageFile#commit} as well).
*/
void rollback() throws IOException {
// release header
file.discard(headerBuf);
file.rollback();
// and obtain it again
headerBuf = file.get(0);
headerBuf.ensureHeapBuffer();
headerBuf.fileHeaderCheckHead(headerBuf.readShort(0) == 0);
}
/**
* Closes the page manager. This flushes the page manager and releases
* the lock on the headerBuf.fileHeader
*/
void close() throws IOException {
file.release(headerBuf);
file.commit();
headerBuf = null;
}
/**
* PageManager permanently locks zero page, and we need this for backups
*/
ByteBuffer getHeaderBufData() {
return headerBuf.getData();
}
public PageIo getFileHeader() {
return headerBuf;
}
}

View File

@ -0,0 +1,329 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import javax.crypto.Cipher;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
/**
* This class manages the transaction log that belongs to every
* {@link PageFile}. The transaction log is either clean, or
* in progress. In the latter case, the transaction manager
* takes care of a roll forward.
*/
// TODO: Handle the case where we are recovering lg9 and lg0, were we
// should start with lg9 instead of lg0!
final class PageTransactionManager {
private PageFile owner;
// streams for transaction log.
private DataOutputStream oos;
/**
* In-core copy of transactions. We could read everything back from
* the log file, but the PageFile needs to keep the dirty pages in
* core anyway, so we might as well point to them and spare us a lot
* of hassle.
*/
private ArrayList<PageIo> txn = new ArrayList<PageIo>();
private int curTxn = -1;
private Storage storage;
private Cipher cipherIn;
private Cipher cipherOut;
/**
* Instantiates a transaction manager instance. If recovery
* needs to be performed, it is done.
*
* @param owner the PageFile instance that owns this transaction mgr.
* @param storage
* @param cipherIn
* @param cipherOut
*/
PageTransactionManager(PageFile owner, Storage storage, Cipher cipherIn, Cipher cipherOut) throws IOException {
this.owner = owner;
this.storage = storage;
this.cipherIn = cipherIn;
this.cipherOut = cipherOut;
recover();
open();
}
/**
* Synchronize log file data with the main database file.
* <p/>
* After this call, the main database file is guaranteed to be
* consistent and guaranteed to be the only file needed for
* backup purposes.
*/
public void synchronizeLog()
throws IOException {
synchronizeLogFromMemory();
}
/**
* Synchs in-core transactions to data file and opens a fresh log
*/
private void synchronizeLogFromMemory() throws IOException {
close();
TreeSet<PageIo> pageList = new TreeSet<PageIo>(PAGE_IO_COMPARTOR);
int numPages = 0;
int writtenPages = 0;
if(txn!=null){
// Add each page to the pageList, replacing the old copy of this
// page if necessary, thus avoiding writing the same page twice
for (Iterator<PageIo> k = txn.iterator(); k.hasNext(); ) {
PageIo page = k.next();
if (pageList.contains(page)) {
page.decrementTransactionCount();
} else {
writtenPages++;
boolean result = pageList.add(page);
}
numPages++;
}
txn = null;
}
// Write the page from the pageList to disk
synchronizePages(pageList, true);
owner.sync();
open();
}
/**
* Opens the log file
*/
private void open() throws IOException {
oos = storage.openTransactionLog();
oos.writeShort(Magic.LOGFILE_HEADER);
oos.flush();
curTxn = -1;
}
/**
* Startup recovery on all files
*/
private void recover() throws IOException {
DataInputStream ois = storage.readTransactionLog();
// if transaction log is empty, or does not exist
if (ois == null) return;
while (true) {
ArrayList<PageIo> pages = null;
try {
int size = LongPacker.unpackInt(ois);
pages = new ArrayList<PageIo>(size);
for (int i = 0; i < size; i++) {
PageIo b = new PageIo();
b.readExternal(ois, cipherOut);
pages.add(b);
}
} catch (IOException e) {
// corrupted logfile, ignore rest of transactions
break;
}
synchronizePages(pages, false);
}
owner.sync();
ois.close();
storage.deleteTransactionLog();
}
/**
* Synchronizes the indicated pages with the owner.
*/
private void synchronizePages(Iterable<PageIo> pages, boolean fromCore)
throws IOException {
// write pages vector elements to the data file.
for (PageIo cur : pages) {
owner.synch(cur);
if (fromCore) {
cur.decrementTransactionCount();
if (!cur.isInTransaction()) {
owner.releaseFromTransaction(cur);
}
}
}
}
/**
* Set clean flag on the pages.
*/
private void setClean(ArrayList<PageIo> pages)
throws IOException {
for (PageIo cur : pages) {
cur.setClean();
}
}
/**
* Discards the indicated pages and notify the owner.
*/
private void discardPages(ArrayList<PageIo> pages)
throws IOException {
for (PageIo cur : pages) {
cur.decrementTransactionCount();
if (!cur.isInTransaction()) {
owner.releaseFromTransaction(cur);
}
}
}
/**
* Starts a transaction. This can pages if all slots have been filled
* with full transactions, waiting for the synchronization thread to
* clean out slots.
*/
void start() throws IOException {
curTxn++;
if (curTxn == 1) {
synchronizeLogFromMemory();
curTxn = 0;
}
txn = new ArrayList();
}
/**
* Indicates the page is part of the transaction.
*/
void add(PageIo page) throws IOException {
page.incrementTransactionCount();
txn.add(page);
}
/**
* Commits the transaction to the log file.
*/
void commit() throws IOException {
LongPacker.packInt(oos, txn.size());
for (PageIo page : txn) {
page.writeExternal(oos, cipherIn);
}
sync();
// set clean flag to indicate pages have been written to log
setClean(txn);
// open a new ObjectOutputStream in order to store
// newer states of PageIo
// oos = new DataOutputStream(new BufferedOutputStream(fos));
}
/**
* Flushes and syncs
*/
private void sync() throws IOException {
oos.flush();
}
/**
* Shutdowns the transaction manager. Resynchronizes outstanding
* logs.
*/
void shutdown() throws IOException {
synchronizeLogFromMemory();
close();
}
/**
* Closes open files.
*/
private void close() throws IOException {
sync();
oos.close();
oos = null;
}
/**
* Force closing the file without synchronizing pending transaction data.
* Used for testing purposes only.
*/
void forceClose() throws IOException {
oos.close();
oos = null;
}
/**
* Use the disk-based transaction log to synchronize the data file.
* Outstanding memory logs are discarded because they are believed
* to be inconsistent.
*/
void synchronizeLogFromDisk() throws IOException {
close();
if (txn != null){
discardPages(txn);
txn = null;
}
recover();
open();
}
/**
* INNER CLASS.
* Comparator class for use by the tree set used to store the pages
* to write for this transaction. The PageIo objects are ordered by
* their page ids.
*/
private static final Comparator<PageIo> PAGE_IO_COMPARTOR = new Comparator<PageIo>() {
public int compare(PageIo page1, PageIo page2) {
if (page1.getPageId() == page2.getPageId()) {
return 0;
} else if (page1.getPageId() < page2.getPageId()) {
return -1;
} else {
return 1;
}
}
};
}

View File

@ -0,0 +1,209 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.IOException;
import java.util.Arrays;
/**
* This class manages free physical rowid pages and provides methods to free and allocate physical rowids on a high
* level.
*/
final class PhysicalFreeRowIdManager {
/** maximal record size which can be hold. If record crosses multiple pages, it is trimmed before added to free list */
static final int MAX_REC_SIZE = Storage.PAGE_SIZE *2;
/** where data on root page starts, there are no extra data in page header */
static final int ROOT_HEADER_SIZE = Magic.PAGE_HEADER_SIZE;
/** page header size for slot page */
static final int SLOT_PAGE_HEADER_SIZE = Magic.PAGE_HEADER_SIZE + Magic.SZ_SHORT + Magic.SZ_SIX_BYTE_LONG;
/** number of recids on slot page */
static final int OFFSET_SLOT_PAGE_REC_COUNT = Magic.PAGE_HEADER_SIZE;
static final int SLOT_PAGE_REC_NUM = (Storage.PAGE_SIZE - SLOT_PAGE_HEADER_SIZE)/6;
/** pointer to next slo page in slot page header */
static final int OFFSET_SLOT_PAGE_NEXT = Magic.PAGE_HEADER_SIZE + Magic.SZ_SHORT;
/** number of size slots held in root page */
static final int MAX_RECIDS_PER_PAGE = (Storage.PAGE_SIZE -ROOT_HEADER_SIZE-6) / 6; //6 is size of page pointer
/** free records are grouped into slots by record size. Here is max diff in record size per group */
static final int ROOT_SLOT_SIZE = 1+MAX_REC_SIZE/ MAX_RECIDS_PER_PAGE;
protected final PageFile file;
protected final PageManager pageman;
/** list of free phys slots in current transaction. First two bytes are size, last 6 bytes are recid*/
private long[] inTrans = new long[8];
private int inTransSize = 0;
/**
* Creates a new instance using the indicated record file and page manager.
*/
PhysicalFreeRowIdManager(PageFile file, PageManager pageman) throws IOException {
this.file = file;
this.pageman = pageman;
}
long getFreeRecord(final int size) throws IOException {
if(size >= MAX_REC_SIZE) return 0;
final PageIo root = getRootPage();
final int rootPageOffset = sizeToRootOffset(size+ ROOT_SLOT_SIZE);
final long slotPageId = root.readSixByteLong(rootPageOffset);
if(slotPageId==0){
file.release(root);
return 0;
}
PageIo slotPage = file.get(slotPageId);
if(slotPage.readShort(Magic.PAGE_HEADER_O_MAGIC) != Magic.PAGE_MAGIC + Magic.FREEPHYSIDS_PAGE)
throw new InternalError();
short recidCount = slotPage.readShort(OFFSET_SLOT_PAGE_REC_COUNT);
if(recidCount<=0){
throw new InternalError();
}
final int offset = (recidCount-1) * 6 + SLOT_PAGE_HEADER_SIZE;
final long recid = slotPage.readSixByteLong(offset);
recidCount --;
if(recidCount>0){
//decrease counter and zero out old record
slotPage.writeSixByteLong(offset,0);
slotPage.writeShort(OFFSET_SLOT_PAGE_REC_COUNT, recidCount);
file.release(root);
file.release(slotPage);
}else{
//release this page
long prevSlotPageId = slotPage.readSixByteLong(OFFSET_SLOT_PAGE_NEXT);
root.writeSixByteLong(rootPageOffset,prevSlotPageId);
file.release(root);
file.release(slotPage);
pageman.free(Magic.FREEPHYSIDS_PAGE,slotPageId);
}
return recid;
}
static final int sizeToRootOffset(int size) {
return ROOT_HEADER_SIZE + 6 * (size/ROOT_SLOT_SIZE);
}
/**
* Puts the indicated rowid on the free list, which awaits for commit
*/
void putFreeRecord(final long rowid, final int size) throws IOException {
//ensure capacity
if(inTransSize==inTrans.length){
inTrans = Arrays.copyOf(inTrans, inTrans.length * 2);
}
inTrans[inTransSize] = rowid + (((long)size)<<48);
inTransSize++;
}
public void commit() throws IOException {
if(inTransSize==0)
return;
Arrays.sort(inTrans,0,inTransSize-1);
//write all uncommited free records
final PageIo root = getRootPage();
PageIo slotPage = null;
for(int rowIdPos = 0; rowIdPos<inTransSize; rowIdPos++){
final int size = (int) (inTrans[rowIdPos] >>>48);
final long rowid = inTrans[rowIdPos] & 0x0000FFFFFFFFFFFFL;
final int rootPageOffset = sizeToRootOffset(size);
long slotPageId = root.readSixByteLong(rootPageOffset);
if(slotPageId == 0){
if(slotPage!=null) file.release(slotPage);
//create new page for this slot
slotPageId = pageman.allocate(Magic.FREEPHYSIDS_PAGE);
root.writeSixByteLong(rootPageOffset,slotPageId);
}
if(slotPage == null || slotPage.getPageId()!=slotPageId){
if(slotPage!=null) file.release(slotPage);
slotPage = file.get(slotPageId);
}
if(slotPage.readShort(Magic.PAGE_HEADER_O_MAGIC) != Magic.PAGE_MAGIC + Magic.FREEPHYSIDS_PAGE)
throw new InternalError();
short recidCount = slotPage.readShort(OFFSET_SLOT_PAGE_REC_COUNT);
if(recidCount== MAX_RECIDS_PER_PAGE){
file.release(slotPage);
//allocate new slot page and update links
final long newSlotPageId = pageman.allocate(Magic.FREEPHYSIDS_PAGE);
slotPage = file.get(newSlotPageId);
slotPage.writeSixByteLong(OFFSET_SLOT_PAGE_NEXT,slotPageId);
slotPage.writeShort(OFFSET_SLOT_PAGE_REC_COUNT,(short)0);
recidCount = 0;
slotPageId = newSlotPageId;
root.writeSixByteLong(rootPageOffset,newSlotPageId);
}
//write new recid
slotPage.writeSixByteLong(recidCount * 6 + SLOT_PAGE_HEADER_SIZE,rowid);
//and increase count
recidCount++;
slotPage.writeShort(OFFSET_SLOT_PAGE_REC_COUNT,recidCount);
}
if(slotPage!=null)
file.release(slotPage);
file.release(root);
clearFreeInTrans();
}
public void rollback() {
clearFreeInTrans();
}
private void clearFreeInTrans() {
if(inTrans.length>128)
inTrans = new long[8];
inTransSize = 0;
}
/** return free phys row page. If not found create it */
final PageIo getRootPage() throws IOException {
long pageId = pageman.getFirst(Magic.FREEPHYSIDS_ROOT_PAGE);
if(pageId == 0){
pageId = pageman.allocate(Magic.FREEPHYSIDS_ROOT_PAGE);
}
return file.get(pageId);
}
}

View File

@ -0,0 +1,354 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.IOException;
import static org.apache.jdbm.Storage.*;
/**
* This class manages physical row ids, and their data.
*/
final class PhysicalRowIdManager {
// The file we're talking to and the associated page manager.
final private PageFile file;
final private PageManager pageman;
final PhysicalFreeRowIdManager freeman;
static final private short DATA_PER_PAGE = (short) (PAGE_SIZE - Magic.DATA_PAGE_O_DATA);
//caches offset after last allocation. So we dont have to iterate throw page every allocation
private long cachedLastAllocatedRecordPage = Long.MIN_VALUE;
private short cachedLastAllocatedRecordOffset = Short.MIN_VALUE;
/**
* Creates a new rowid manager using the indicated record file. and page manager.
*/
PhysicalRowIdManager(PageFile file, PageManager pageManager) throws IOException {
this.file = file;
this.pageman = pageManager;
this.freeman = new PhysicalFreeRowIdManager(file, pageManager);
}
/**
* Inserts a new record. Returns the new physical rowid.
*/
long insert(final byte[] data, final int start, final int length) throws IOException {
if (length < 1)
throw new IllegalArgumentException("Length is <1");
if (start < 0)
throw new IllegalArgumentException("negative start");
long retval = alloc(length);
write(retval, data, start, length);
return retval;
}
/**
* Updates an existing record. Returns the possibly changed physical rowid.
*/
long update(long rowid, final byte[] data, final int start, final int length) throws IOException {
// fetch the record header
PageIo page = file.get(rowid>>> Storage.PAGE_SIZE_SHIFT);
short head = (short) (rowid & Storage.OFFSET_MASK);
int availSize = RecordHeader.getAvailableSize(page, head);
if (length > availSize ||
//difference between free and available space can be only 254.
//if bigger, need to realocate and free page
availSize - length > RecordHeader.MAX_SIZE_SPACE
) {
// not enough space - we need to copy to a new rowid.
file.release(page);
free(rowid);
rowid = alloc(length);
} else {
file.release(page);
}
// 'nuff space, write it in and return the rowid.
write(rowid, data, start, length);
return rowid;
}
void fetch(final DataInputOutput out, final long rowid) throws IOException {
// fetch the record header
long current = rowid >>> Storage.PAGE_SIZE_SHIFT;
PageIo page = file.get(current);
final short head = (short) (rowid & Storage.OFFSET_MASK);
// allocate a return buffer
// byte[] retval = new byte[ head.getCurrentSize() ];
final int size = RecordHeader.getCurrentSize(page, head);
if (size == 0) {
file.release(current, false);
return;
}
// copy bytes in
int leftToRead = size;
short dataOffset = (short) ( head + RecordHeader.SIZE);
while (leftToRead > 0) {
// copy current page's data to return buffer
int toCopy = PAGE_SIZE - dataOffset;
if (leftToRead < toCopy) {
toCopy = leftToRead;
}
out.writeFromByteBuffer(page.getData(), dataOffset, toCopy);
// Go to the next page
leftToRead -= toCopy;
// out.flush();
file.release(page);
if (leftToRead > 0) {
current = pageman.getNext(current);
page = file.get(current);
dataOffset = Magic.DATA_PAGE_O_DATA;
}
}
// return retval;
}
/**
* Allocate a new rowid with the indicated size.
*/
private long alloc(int size) throws IOException {
size = RecordHeader.roundAvailableSize(size);
long retval = freeman.getFreeRecord(size);
if (retval == 0) {
retval = allocNew(size, pageman.getLast(Magic.USED_PAGE));
}
return retval;
}
/**
* Allocates a new rowid. The second parameter is there to allow for a recursive call - it indicates where the
* search should start.
*/
private long allocNew(int size, long start) throws IOException {
PageIo curPage;
if (start == 0 ||
//last page was completely filled?
cachedLastAllocatedRecordPage == start && cachedLastAllocatedRecordOffset == PAGE_SIZE
) {
// we need to create a new page.
start = pageman.allocate(Magic.USED_PAGE);
curPage = file.get(start);
curPage.dataPageSetFirst(Magic.DATA_PAGE_O_DATA);
cachedLastAllocatedRecordOffset = Magic.DATA_PAGE_O_DATA;
cachedLastAllocatedRecordPage = curPage.getPageId();
RecordHeader.setAvailableSize(curPage, Magic.DATA_PAGE_O_DATA, 0);
RecordHeader.setCurrentSize(curPage, Magic.DATA_PAGE_O_DATA, 0);
} else {
curPage = file.get(start);
}
// follow the rowids on this page to get to the last one. We don't
// fall off, because this is the last page, remember?
short pos = curPage.dataPageGetFirst();
if (pos == 0) {
// page is exactly filled by the last page of a record
file.release(curPage);
return allocNew(size, 0);
}
short hdr = pos;
if (cachedLastAllocatedRecordPage != curPage.getPageId() ) {
//position was not cached, have to find it again
int availSize = RecordHeader.getAvailableSize(curPage, hdr);
while (availSize != 0 && pos < PAGE_SIZE) {
pos += availSize + RecordHeader.SIZE;
if (pos == PAGE_SIZE) {
// Again, a filled page.
file.release(curPage);
return allocNew(size, 0);
}
hdr = pos;
availSize = RecordHeader.getAvailableSize(curPage, hdr);
}
} else {
hdr = cachedLastAllocatedRecordOffset;
pos = cachedLastAllocatedRecordOffset;
}
if (pos == RecordHeader.SIZE) { //TODO why is this here?
// the last record exactly filled the page. Restart forcing
// a new page.
file.release(curPage);
}
if(hdr>Storage.PAGE_SIZE - 16){
file.release(curPage);
//there is not enought space on current page, so force new page
return allocNew(size,0);
}
// we have the position, now tack on extra pages until we've got
// enough space.
long retval =(start << Storage.PAGE_SIZE_SHIFT) + (long) pos;
int freeHere = PAGE_SIZE - pos - RecordHeader.SIZE;
if (freeHere < size) {
// check whether the last page would have only a small bit left.
// if yes, increase the allocation. A small bit is a record
// header plus 16 bytes.
int lastSize = (size - freeHere) % DATA_PER_PAGE;
if (size <DATA_PER_PAGE && (DATA_PER_PAGE - lastSize) < (RecordHeader.SIZE + 16)) {
size += (DATA_PER_PAGE - lastSize);
size = RecordHeader.roundAvailableSize(size);
}
// write out the header now so we don't have to come back.
RecordHeader.setAvailableSize(curPage, hdr, size);
file.release(start, true);
int neededLeft = size - freeHere;
// Refactor these two pages!
while (neededLeft >= DATA_PER_PAGE) {
start = pageman.allocate(Magic.USED_PAGE);
curPage = file.get(start);
curPage.dataPageSetFirst((short) 0); // no rowids, just data
file.release(start, true);
neededLeft -= DATA_PER_PAGE;
}
if (neededLeft > 0) {
// done with whole chunks, allocate last fragment.
start = pageman.allocate(Magic.USED_PAGE);
curPage = file.get(start);
curPage.dataPageSetFirst((short) (Magic.DATA_PAGE_O_DATA + neededLeft));
file.release(start, true);
cachedLastAllocatedRecordOffset = (short) (Magic.DATA_PAGE_O_DATA + neededLeft);
cachedLastAllocatedRecordPage = curPage.getPageId();
}
} else {
// just update the current page. If there's less than 16 bytes
// left, we increase the allocation (16 bytes is an arbitrary
// number).
if (freeHere - size <= (16 + RecordHeader.SIZE)) {
size = freeHere;
}
RecordHeader.setAvailableSize(curPage, hdr, size);
file.release(start, true);
cachedLastAllocatedRecordOffset = (short) (hdr + RecordHeader.SIZE + size);
cachedLastAllocatedRecordPage = curPage.getPageId();
}
return retval;
}
void free(final long id) throws IOException {
// get the rowid, and write a zero current size into it.
final long curPageId = id >>> Storage.PAGE_SIZE_SHIFT;
final PageIo curPage = file.get(curPageId);
final short offset = (short) (id & Storage.OFFSET_MASK);
RecordHeader.setCurrentSize(curPage, offset, 0);
int size = RecordHeader.getAvailableSize(curPage, offset);
//trim size if spreads across multiple pages
if(offset + RecordHeader.SIZE + size >PAGE_SIZE + (PAGE_SIZE-Magic.DATA_PAGE_O_DATA)){
int numOfPagesToSkip = (size -
(Storage.PAGE_SIZE-(offset - RecordHeader.SIZE)) //minus data remaining on this page
)/(PAGE_SIZE-Magic.DATA_PAGE_O_DATA);
size = size - numOfPagesToSkip * (PAGE_SIZE-Magic.DATA_PAGE_O_DATA);
RecordHeader.setAvailableSize(curPage, offset,size);
//get next page
long nextPage = curPage.pageHeaderGetNext();
file.release(curPage);
//release pages
for(int i = 0;i<numOfPagesToSkip;i++){
PageIo page = file.get(nextPage);
long nextPage2 = page.pageHeaderGetNext();
file.release(page);
pageman.free(Magic.USED_PAGE,nextPage);
nextPage = nextPage2;
}
}else{
file.release(curPage);
}
// write the rowid to the free list
freeman.putFreeRecord(id, size);
}
/**
* Writes out data to a rowid. Assumes that any resizing has been done.
*/
private void write(final long rowid, final byte[] data,final int start, final int length) throws IOException {
long current = rowid >>> Storage.PAGE_SIZE_SHIFT;
PageIo page = file.get(current);
final short hdr = (short) (rowid & Storage.OFFSET_MASK);
RecordHeader.setCurrentSize(page, hdr, length);
if (length == 0) {
file.release(current, true);
return;
}
// copy bytes in
int offsetInBuffer = start;
int leftToWrite = length;
short dataOffset = (short) (hdr + RecordHeader.SIZE);
while (leftToWrite > 0) {
// copy current page's data to return buffer
int toCopy = PAGE_SIZE - dataOffset;
if (leftToWrite < toCopy) {
toCopy = leftToWrite;
}
page.writeByteArray(data, offsetInBuffer, dataOffset, toCopy);
// Go to the next page
leftToWrite -= toCopy;
offsetInBuffer += toCopy;
file.release(current, true);
if (leftToWrite > 0) {
current = pageman.getNext(current);
page = file.get(current);
dataOffset = Magic.DATA_PAGE_O_DATA;
}
}
}
void rollback() throws IOException {
cachedLastAllocatedRecordPage = Long.MIN_VALUE;
cachedLastAllocatedRecordOffset = Short.MIN_VALUE;
freeman.rollback();
}
void commit() throws IOException {
freeman.commit();
}
}

View File

@ -0,0 +1,123 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
/**
* The data that comes at the start of a record of data. It stores
* both the current size and the avaliable size for the record - the latter
* can be bigger than the former, which allows the record to grow without
* needing to be moved and which allows the system to put small records
* in larger free spots.
* <p/>
* In JDBM 1.0 both values were stored as four-byte integers. This was very wastefull.
* Now available size is stored in two bytes, it is compressed, so maximal value is up to 120 MB (not sure with exact number)
* Current size is stored as two-byte-unsigned-short difference from Available Size.
*/
final class RecordHeader {
// offsets
private static final short O_CURRENTSIZE = 0; // int currentSize
private static final short O_AVAILABLESIZE = Magic.SZ_BYTE; // int availableSize
static final int MAX_RECORD_SIZE = 8355839;
static final int SIZE = O_AVAILABLESIZE + Magic.SZ_SHORT;
/**
* Maximal difference between current and available size,
* Maximal value is reserved for currentSize 0, so use -1
*/
static final int MAX_SIZE_SPACE = 255 - 1;
/**
* Returns the current size
*/
static int getCurrentSize(final PageIo page, final short pos) {
int s = page.readByte(pos + O_CURRENTSIZE) & 0xFF;
if (s == MAX_SIZE_SPACE + 1)
return 0;
return getAvailableSize(page, pos) - s;
}
/**
* Sets the current size
*/
static void setCurrentSize(final PageIo page, final short pos, int value) {
if (value == 0) {
page.writeByte(pos + O_CURRENTSIZE, (byte) (MAX_SIZE_SPACE + 1));
return;
}
int availSize = getAvailableSize(page, pos);
if (value < (availSize - MAX_SIZE_SPACE) || value > availSize)
throw new IllegalArgumentException("currentSize out of bounds, need to realocate " + value + " - " + availSize);
page.writeByte(pos + O_CURRENTSIZE, (byte) (availSize - value));
}
/**
* Returns the available size
*/
static int getAvailableSize(final PageIo page, final short pos) {
return deconvertAvailSize(page.readShort(pos + O_AVAILABLESIZE));
}
/**
* Sets the available size
*/
static void setAvailableSize(final PageIo page, final short pos, int value) {
if (value != roundAvailableSize(value))
throw new IllegalArgumentException("value is not rounded");
int oldCurrSize = getCurrentSize(page, pos);
page.writeShort(pos + O_AVAILABLESIZE, convertAvailSize(value));
setCurrentSize(page, pos, oldCurrSize);
}
static short convertAvailSize(final int recordSize) {
if (recordSize <= Short.MAX_VALUE)
return (short) recordSize;
else {
int shift = recordSize - Short.MAX_VALUE;
if (shift % MAX_SIZE_SPACE == 0)
shift = shift / MAX_SIZE_SPACE;
else
shift = 1 + shift / MAX_SIZE_SPACE;
shift = -shift;
return (short) (shift);
}
}
static int deconvertAvailSize(final short converted) {
if (converted >= 0)
return converted;
else {
int shifted = -converted;
shifted = shifted * MAX_SIZE_SPACE;
return Short.MAX_VALUE + shifted;
}
}
static int roundAvailableSize(int value) {
if (value > MAX_RECORD_SIZE)
new InternalError("Maximal record size (" + MAX_RECORD_SIZE + ") exceeded: " + value);
return deconvertAvailSize(convertAvailSize(value));
}
}

View File

@ -0,0 +1,38 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.IOException;
/**
* An listener notifed when record is inserted, updated or removed.
* <p/>
* NOTE: this class was used in JDBM2 to support secondary indexes
* JDBM3 does not have a secondary indexes, so this class is not publicly exposed.
*
* @param <K> key type
* @param <V> value type
* @author Jan Kotek
*/
interface RecordListener<K, V> {
void recordInserted(K key, V value) throws IOException;
void recordUpdated(K key, V oldValue, V newValue) throws IOException;
void recordRemoved(K key, V value) throws IOException;
}

View File

@ -0,0 +1,533 @@
package org.apache.jdbm;
import org.apache.jdbm.Serialization.FastArrayList;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class stores information about serialized classes and fields.
*/
abstract class SerialClassInfo {
static final Serializer<ArrayList<ClassInfo>> serializer = new Serializer<ArrayList<ClassInfo>>() {
public void serialize(DataOutput out, ArrayList<ClassInfo> obj) throws IOException {
LongPacker.packInt(out, obj.size());
for (ClassInfo ci : obj) {
out.writeUTF(ci.getName());
out.writeBoolean(ci.isEnum);
out.writeBoolean(ci.isExternalizable);
if(ci.isExternalizable) continue; //no fields
LongPacker.packInt(out, ci.fields.size());
for (FieldInfo fi : ci.fields) {
out.writeUTF(fi.getName());
out.writeBoolean(fi.isPrimitive());
out.writeUTF(fi.getType());
}
}
}
public ArrayList<ClassInfo> deserialize(DataInput in) throws IOException, ClassNotFoundException {
int size = LongPacker.unpackInt(in);
ArrayList<ClassInfo> ret = new ArrayList<ClassInfo>(size);
for (int i = 0; i < size; i++) {
String className = in.readUTF();
boolean isEnum = in.readBoolean();
boolean isExternalizable = in.readBoolean();
int fieldsNum = isExternalizable? 0 : LongPacker.unpackInt(in);
FieldInfo[] fields = new FieldInfo[fieldsNum];
for (int j = 0; j < fieldsNum; j++) {
fields[j] = new FieldInfo(in.readUTF(), in.readBoolean(), in.readUTF(), Class.forName(className));
}
ret.add(new ClassInfo(className, fields,isEnum,isExternalizable));
}
return ret;
}
};
long serialClassInfoRecid;
public SerialClassInfo(DBAbstract db, long serialClassInfoRecid, ArrayList<ClassInfo> registered){
this.db = db;
this.serialClassInfoRecid = serialClassInfoRecid;
this.registered = registered;
}
/**
* Stores info about single class stored in JDBM.
* Roughly corresponds to 'java.io.ObjectStreamClass'
*/
static class ClassInfo {
private final String name;
private final List<FieldInfo> fields = new ArrayList<FieldInfo>();
private final Map<String, FieldInfo> name2fieldInfo = new HashMap<String, FieldInfo>();
private final Map<String, Integer> name2fieldId = new HashMap<String, Integer>();
private ObjectStreamField[] objectStreamFields;
final boolean isEnum;
final boolean isExternalizable;
ClassInfo(final String name, final FieldInfo[] fields, final boolean isEnum, final boolean isExternalizable) {
this.name = name;
this.isEnum = isEnum;
this.isExternalizable = isExternalizable;
for (FieldInfo f : fields) {
this.name2fieldId.put(f.getName(), this.fields.size());
this.fields.add(f);
this.name2fieldInfo.put(f.getName(), f);
}
}
public String getName() {
return name;
}
public FieldInfo[] getFields() {
return (FieldInfo[]) fields.toArray();
}
public FieldInfo getField(String name) {
return name2fieldInfo.get(name);
}
public int getFieldId(String name) {
Integer fieldId = name2fieldId.get(name);
if(fieldId != null)
return fieldId;
return -1;
}
public FieldInfo getField(int serialId) {
return fields.get(serialId);
}
public int addFieldInfo(FieldInfo field) {
name2fieldId.put(field.getName(), fields.size());
name2fieldInfo.put(field.getName(), field);
fields.add(field);
return fields.size() - 1;
}
public ObjectStreamField[] getObjectStreamFields() {
return objectStreamFields;
}
public void setObjectStreamFields(ObjectStreamField[] objectStreamFields) {
this.objectStreamFields = objectStreamFields;
}
}
/**
* Stores info about single field stored in JDBM.
* Roughly corresponds to 'java.io.ObjectFieldClass'
*/
static class FieldInfo {
private final String name;
private final boolean primitive;
private final String type;
private Class typeClass;
// Class containing this field
private final Class clazz;
private Object setter;
private int setterIndex;
private Object getter;
private int getterIndex;
public FieldInfo(String name, boolean primitive, String type, Class clazz) {
this.name = name;
this.primitive = primitive;
this.type = type;
this.clazz = clazz;
try {
this.typeClass = Class.forName(type);
} catch (ClassNotFoundException e) {
this.typeClass = null;
}
initSetter();
initGetter();
}
private void initSetter() {
// Set setter
String setterName = "set" + firstCharCap(name);
String fieldSetterName = clazz.getName() + "#" + setterName;
Class aClazz = clazz;
// iterate over class hierarchy, until root class
while (aClazz != Object.class) {
// check if there is getMethod
try {
Method m = aClazz.getMethod(setterName, typeClass);
if (m != null) {
setter = m;
return;
}
} catch (Exception e) {
// e.printStackTrace();
}
// no get method, access field directly
try {
Field f = aClazz.getDeclaredField(name);
// security manager may not be happy about this
if (!f.isAccessible())
f.setAccessible(true);
setter = f;
return;
} catch (Exception e) {
// e.printStackTrace();
}
// move to superclass
aClazz = aClazz.getSuperclass();
}
}
private void initGetter() {
// Set setter
String getterName = "get" + firstCharCap(name);
String fieldSetterName = clazz.getName() + "#" + getterName;
Class aClazz = clazz;
// iterate over class hierarchy, until root class
while (aClazz != Object.class) {
// check if there is getMethod
try {
Method m = aClazz.getMethod(getterName);
if (m != null) {
getter = m;
return;
}
} catch (Exception e) {
// e.printStackTrace();
}
// no get method, access field directly
try {
Field f = aClazz.getDeclaredField(name);
// security manager may not be happy about this
if (!f.isAccessible())
f.setAccessible(true);
getter = f;
return;
} catch (Exception e) {
// e.printStackTrace();
}
// move to superclass
aClazz = aClazz.getSuperclass();
}
}
public FieldInfo(ObjectStreamField sf, Class clazz) {
this(sf.getName(), sf.isPrimitive(), sf.getType().getName(), clazz);
}
public String getName() {
return name;
}
public boolean isPrimitive() {
return primitive;
}
public String getType() {
return type;
}
private String firstCharCap(String s) {
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
}
ArrayList<ClassInfo> registered;
Map<Class, Integer> class2classId = new HashMap<Class, Integer>();
Map<Integer, Class> classId2class = new HashMap<Integer, Class>();
final DBAbstract db;
public void registerClass(Class clazz) throws IOException {
if(clazz != Object.class)
assertClassSerializable(clazz);
if (containsClass(clazz))
return;
ObjectStreamField[] streamFields = getFields(clazz);
FieldInfo[] fields = new FieldInfo[streamFields.length];
for (int i = 0; i < fields.length; i++) {
ObjectStreamField sf = streamFields[i];
fields[i] = new FieldInfo(sf, clazz);
}
ClassInfo i = new ClassInfo(clazz.getName(), fields,clazz.isEnum(), Externalizable.class.isAssignableFrom(clazz));
class2classId.put(clazz, registered.size());
classId2class.put(registered.size(), clazz);
registered.add(i);
if (db != null)
db.update(serialClassInfoRecid, (Serialization) this, db.defaultSerializationSerializer);
}
private ObjectStreamField[] getFields(Class clazz) {
ObjectStreamField[] fields = null;
ClassInfo classInfo = null;
Integer classId = class2classId.get(clazz);
if (classId != null) {
classInfo = registered.get(classId);
fields = classInfo.getObjectStreamFields();
}
if (fields == null) {
ObjectStreamClass streamClass = ObjectStreamClass.lookup(clazz);
FastArrayList<ObjectStreamField> fieldsList = new FastArrayList<ObjectStreamField>();
while (streamClass != null) {
for (ObjectStreamField f : streamClass.getFields()) {
fieldsList.add(f);
}
clazz = clazz.getSuperclass();
streamClass = ObjectStreamClass.lookup(clazz);
}
fields = new ObjectStreamField[fieldsList
.size()];
for (int i = 0; i < fields.length; i++) {
fields[i] = fieldsList.get(i);
}
if(classInfo != null)
classInfo.setObjectStreamFields(fields);
}
return fields;
}
private void assertClassSerializable(Class clazz) throws NotSerializableException, InvalidClassException {
if(containsClass(clazz))
return;
if (!Serializable.class.isAssignableFrom(clazz))
throw new NotSerializableException(clazz.getName());
}
public Object getFieldValue(String fieldName, Object object) {
try {
registerClass(object.getClass());
} catch (IOException e) {
e.printStackTrace();
}
ClassInfo classInfo = registered.get(class2classId.get(object.getClass()));
return getFieldValue(classInfo.getField(fieldName), object);
}
public Object getFieldValue(FieldInfo fieldInfo, Object object) {
Object fieldAccessor = fieldInfo.getter;
try {
if (fieldAccessor instanceof Method) {
Method m = (Method) fieldAccessor;
return m.invoke(object);
} else {
Field f = (Field) fieldAccessor;
return f.get(object);
}
} catch (Exception e) {
}
throw new NoSuchFieldError(object.getClass() + "." + fieldInfo.getName());
}
public void setFieldValue(String fieldName, Object object, Object value) {
try {
registerClass(object.getClass());
} catch (IOException e) {
e.printStackTrace();
}
ClassInfo classInfo = registered.get(class2classId.get(object.getClass()));
setFieldValue(classInfo.getField(fieldName), object, value);
}
public void setFieldValue(FieldInfo fieldInfo, Object object, Object value) {
Object fieldAccessor = fieldInfo.setter;
try {
if (fieldAccessor instanceof Method) {
Method m = (Method) fieldAccessor;
m.invoke(object, value);
} else {
Field f = (Field) fieldAccessor;
f.set(object, value);
}
return;
} catch (Throwable e) {
e.printStackTrace();
}
throw new NoSuchFieldError(object.getClass() + "." + fieldInfo.getName());
}
public boolean containsClass(Class clazz) {
return (class2classId.get(clazz) != null);
}
public int getClassId(Class clazz) {
Integer classId = class2classId.get(clazz);
if(classId != null) {
return classId;
}
throw new Error("Class is not registered: " + clazz);
}
public void writeObject(DataOutput out, Object obj, FastArrayList objectStack) throws IOException {
registerClass(obj.getClass());
//write class header
int classId = getClassId(obj.getClass());
LongPacker.packInt(out, classId);
ClassInfo classInfo = registered.get(classId);
if(classInfo.isExternalizable){
Externalizable o = (Externalizable) obj;
DataInputOutput out2 = (DataInputOutput) out;
try{
out2.serializer = this;
out2.objectStack = objectStack;
o.writeExternal(out2);
}finally {
out2.serializer = null;
out2.objectStack = null;
}
return;
}
if(classInfo.isEnum) {
int ordinal = ((Enum)obj).ordinal();
LongPacker.packInt(out, ordinal);
}
ObjectStreamField[] fields = getFields(obj.getClass());
LongPacker.packInt(out, fields.length);
for (ObjectStreamField f : fields) {
//write field ID
int fieldId = classInfo.getFieldId(f.getName());
if (fieldId == -1) {
//field does not exists in class definition stored in db,
//propably new field was added so add field descriptor
fieldId = classInfo.addFieldInfo(new FieldInfo(f, obj.getClass()));
db.update(serialClassInfoRecid, (Serialization) this, db.defaultSerializationSerializer);
}
LongPacker.packInt(out, fieldId);
//and write value
Object fieldValue = getFieldValue(classInfo.getField(fieldId), obj);
serialize(out, fieldValue, objectStack);
}
}
public Object readObject(DataInput in, FastArrayList objectStack) throws IOException {
//read class header
try {
int classId = LongPacker.unpackInt(in);
ClassInfo classInfo = registered.get(classId);
// Class clazz = Class.forName(classInfo.getName());
Class clazz = classId2class.get(classId);
if(clazz == null)
clazz = Class.forName(classInfo.getName());
assertClassSerializable(clazz);
Object o;
if(classInfo.isEnum) {
int ordinal = LongPacker.unpackInt(in);
o = clazz.getEnumConstants()[ordinal];
}
else {
o = createInstance(clazz, Object.class);
}
objectStack.add(o);
if(classInfo.isExternalizable){
Externalizable oo = (Externalizable) o;
DataInputOutput in2 = (DataInputOutput) in;
try{
in2.serializer = this;
in2.objectStack = objectStack;
oo.readExternal(in2);
}finally {
in2.serializer = null;
in2.objectStack = null;
}
}else{
int fieldCount = LongPacker.unpackInt(in);
for (int i = 0; i < fieldCount; i++) {
int fieldId = LongPacker.unpackInt(in);
FieldInfo f = classInfo.getField(fieldId);
Object fieldValue = deserialize(in, objectStack);
setFieldValue(f, o, fieldValue);
}
}
return o;
} catch (Exception e) {
throw new Error("Could not instanciate class", e);
}
}
//TODO dependecy on nonpublic JVM API
static private sun.reflect.ReflectionFactory rf =
sun.reflect.ReflectionFactory.getReflectionFactory();
private static Map<Class, Constructor> class2constuctor = new HashMap<Class, Constructor>();
/**
* Little trick to create new instance without using constructor.
* Taken from http://www.javaspecialists.eu/archive/Issue175.html
*/
private static <T> T createInstance(Class<T> clazz, Class<? super T> parent) {
try {
Constructor intConstr = class2constuctor.get(clazz);
if (intConstr == null) {
Constructor objDef = parent.getDeclaredConstructor();
intConstr = rf.newConstructorForSerialization(
clazz, objDef);
class2constuctor.put(clazz, intConstr);
}
return clazz.cast(intConstr.newInstance());
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("Cannot create object", e);
}
}
protected abstract Object deserialize(DataInput in, FastArrayList objectStack) throws IOException, ClassNotFoundException;
protected abstract void serialize(DataOutput out, Object fieldValue, FastArrayList objectStack) throws IOException;
//
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
package org.apache.jdbm;
/**
* Header byte, is used at start of each record to indicate data type
* WARNING !!! values bellow must be unique !!!!!
*/
final class SerializationHeader {
final static int NULL = 0;
final static int NORMAL = 1;
final static int BOOLEAN_TRUE = 2;
final static int BOOLEAN_FALSE = 3;
final static int INTEGER_MINUS_1 = 4;
final static int INTEGER_0 = 5;
final static int INTEGER_1 = 6;
final static int INTEGER_2 = 7;
final static int INTEGER_3 = 8;
final static int INTEGER_4 = 9;
final static int INTEGER_5 = 10;
final static int INTEGER_6 = 11;
final static int INTEGER_7 = 12;
final static int INTEGER_8 = 13;
final static int INTEGER_255 = 14;
final static int INTEGER_PACK_NEG = 15;
final static int INTEGER_PACK = 16;
final static int LONG_MINUS_1 = 17;
final static int LONG_0 = 18;
final static int LONG_1 = 19;
final static int LONG_2 = 20;
final static int LONG_3 = 21;
final static int LONG_4 = 22;
final static int LONG_5 = 23;
final static int LONG_6 = 24;
final static int LONG_7 = 25;
final static int LONG_8 = 26;
final static int LONG_PACK_NEG = 27;
final static int LONG_PACK = 28;
final static int LONG_255 = 29;
final static int LONG_MINUS_MAX = 30;
final static int SHORT_MINUS_1 = 31;
final static int SHORT_0 = 32;
final static int SHORT_1 = 33;
final static int SHORT_255 = 34;
final static int SHORT_FULL = 35;
final static int BYTE_MINUS_1 = 36;
final static int BYTE_0 = 37;
final static int BYTE_1 = 38;
final static int BYTE_FULL = 39;
final static int CHAR = 40;
final static int FLOAT_MINUS_1 = 41;
final static int FLOAT_0 = 42;
final static int FLOAT_1 = 43;
final static int FLOAT_255 = 44;
final static int FLOAT_SHORT = 45;
final static int FLOAT_FULL = 46;
final static int DOUBLE_MINUS_1 = 47;
final static int DOUBLE_0 = 48;
final static int DOUBLE_1 = 49;
final static int DOUBLE_255 = 50;
final static int DOUBLE_SHORT = 51;
final static int DOUBLE_FULL = 52;
final static int DOUBLE_ARRAY = 53;
final static int BIGDECIMAL = 54;
final static int BIGINTEGER = 55;
final static int FLOAT_ARRAY = 56;
final static int INTEGER_MINUS_MAX = 57;
final static int SHORT_ARRAY = 58;
final static int BOOLEAN_ARRAY = 59;
final static int ARRAY_INT_B_255 = 60;
final static int ARRAY_INT_B_INT = 61;
final static int ARRAY_INT_S = 62;
final static int ARRAY_INT_I = 63;
final static int ARRAY_INT_PACKED = 64;
final static int ARRAY_LONG_B = 65;
final static int ARRAY_LONG_S = 66;
final static int ARRAY_LONG_I = 67;
final static int ARRAY_LONG_L = 68;
final static int ARRAY_LONG_PACKED = 69;
final static int CHAR_ARRAY = 70;
final static int ARRAY_BYTE_INT = 71;
final static int NOTUSED_ARRAY_OBJECT_255 = 72;
final static int ARRAY_OBJECT = 73;
//special cases for BTree values which stores references
final static int ARRAY_OBJECT_PACKED_LONG = 74;
final static int ARRAYLIST_PACKED_LONG = 75;
final static int STRING_EMPTY = 101;
final static int NOTUSED_STRING_255 = 102;
final static int STRING = 103;
final static int NOTUSED_ARRAYLIST_255 = 104;
final static int ARRAYLIST = 105;
final static int NOTUSED_TREEMAP_255 = 106;
final static int TREEMAP = 107;
final static int NOTUSED_HASHMAP_255 = 108;
final static int HASHMAP = 109;
final static int NOTUSED_LINKEDHASHMAP_255 = 110;
final static int LINKEDHASHMAP = 111;
final static int NOTUSED_TREESET_255 = 112;
final static int TREESET = 113;
final static int NOTUSED_HASHSET_255 = 114;
final static int HASHSET = 115;
final static int NOTUSED_LINKEDHASHSET_255 = 116;
final static int LINKEDHASHSET = 117;
final static int NOTUSED_LINKEDLIST_255 = 118;
final static int LINKEDLIST = 119;
final static int NOTUSED_VECTOR_255 = 120;
final static int VECTOR = 121;
final static int IDENTITYHASHMAP = 122;
final static int HASHTABLE = 123;
final static int LOCALE = 124;
final static int PROPERTIES = 125;
final static int CLASS = 126;
final static int DATE = 127;
final static int UUID = 128;
static final int JDBMLINKEDLIST = 159;
static final int HTREE = 160;
final static int BTREE = 161;
static final int BTREE_NODE_LEAF = 162;
static final int BTREE_NODE_NONLEAF = 163;
static final int HTREE_BUCKET = 164;
static final int HTREE_DIRECTORY = 165;
/**
* used for reference to already serialized object in object graph
*/
static final int OBJECT_STACK = 166;
static final int JAVA_SERIALIZATION = 172;
}

View File

@ -0,0 +1,51 @@
/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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 org.apache.jdbm;
import java.io.*;
/**
* Interface used to provide a serialization mechanism other than a class' normal
* serialization.
*
* @author Alex Boisvert
*/
public interface Serializer<A> {
/**
* Serialize the content of an object into a byte array.
*
* @param out ObjectOutput to save object into
* @param obj Object to serialize
*/
public void serialize(DataOutput out, A obj)
throws IOException;
/**
* Deserialize the content of an object from a byte array.
*
* @param in to read serialized data from
* @return deserialized object
* @throws IOException
* @throws ClassNotFoundException
*/
public A deserialize(DataInput in)
throws IOException, ClassNotFoundException;
}

View File

@ -0,0 +1,54 @@
package org.apache.jdbm;
import java.io.*;
import java.nio.ByteBuffer;
/**
*
*/
interface Storage {
/**
* Bite shift used to calculate page size.
* If you want to modify page size, do it here.
*
* 1<<9 = 512
* 1<<10 = 1024
* 1<<11 = 2048
* 1<<12 = 4096
*/
int PAGE_SIZE_SHIFT = 12;
/**
* the lenght of single page.
* <p>
*!!! DO NOT MODIFY THI DIRECTLY !!!
*/
int PAGE_SIZE = 1<< PAGE_SIZE_SHIFT;
/**
* use 'val & OFFSET_MASK' to quickly get offset within the page;
*/
long OFFSET_MASK = 0xFFFFFFFFFFFFFFFFL >>> (64-Storage.PAGE_SIZE_SHIFT);
void write(long pageNumber, ByteBuffer data) throws IOException;
ByteBuffer read(long pageNumber) throws IOException;
void forceClose() throws IOException;
boolean isReadonly();
DataInputStream readTransactionLog();
void deleteTransactionLog();
void sync() throws IOException;
DataOutputStream openTransactionLog() throws IOException;
void deleteAllFiles() throws IOException;
}

View File

@ -0,0 +1,192 @@
package org.apache.jdbm;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.OverlappingFileLockException;
import java.util.ArrayList;
import java.util.List;
import static org.apache.jdbm.StorageDiskMapped.*;
/**
* Storage which used files on disk to store data
*/
class StorageDisk implements Storage {
private ArrayList<RandomAccessFile> rafs = new ArrayList<RandomAccessFile>();
private ArrayList<RandomAccessFile> rafsTranslation = new ArrayList<RandomAccessFile>();
private String fileName;
private long lastPageNumber = Long.MIN_VALUE;
private boolean readonly;
private boolean lockingDisabled;
public StorageDisk(String fileName,boolean readonly, boolean lockingDisabled) throws IOException {
this.fileName = fileName;
this.readonly = readonly;
this.lockingDisabled = lockingDisabled;
//make sure first file can be opened
//lock it
try {
if(!readonly && !lockingDisabled)
getRaf(0).getChannel().tryLock();
} catch (IOException e) {
throw new IOException("Could not lock DB file: " + fileName, e);
} catch (OverlappingFileLockException e) {
throw new IOException("Could not lock DB file: " + fileName, e);
}
}
RandomAccessFile getRaf(long pageNumber) throws IOException {
int fileNumber = (int) (Math.abs(pageNumber)/PAGES_PER_FILE );
List<RandomAccessFile> c = pageNumber>=0 ? rafs : rafsTranslation;
//increase capacity of array lists if needed
for (int i = c.size(); i <= fileNumber; i++) {
c.add(null);
}
RandomAccessFile ret = c.get(fileNumber);
if (ret == null) {
String name = StorageDiskMapped.makeFileName(fileName, pageNumber, fileNumber);
ret = new RandomAccessFile(name, readonly?"r":"rw");
c.set(fileNumber, ret);
}
return ret;
}
public void write(long pageNumber, ByteBuffer data) throws IOException {
if (data.capacity() != PAGE_SIZE) throw new IllegalArgumentException();
long offset = pageNumber * PAGE_SIZE;
RandomAccessFile file = getRaf(pageNumber);
// if (lastPageNumber + 1 != pageNumber) //TODO cache position again, so seek is not necessary
file.seek(Math.abs(offset % (PAGES_PER_FILE* PAGE_SIZE)));
file.write(data.array());
lastPageNumber = pageNumber;
}
public ByteBuffer read(long pageNumber) throws IOException {
long offset = pageNumber * PAGE_SIZE;
ByteBuffer buffer = ByteBuffer.allocate(PAGE_SIZE);
RandomAccessFile file = getRaf(pageNumber);
// if (lastPageNumber + 1 != pageNumber) //TODO cache position again, so seek is not necessary
file.seek(Math.abs(offset % (PAGES_PER_FILE* PAGE_SIZE)));
int remaining = buffer.limit();
int pos = 0;
while (remaining > 0) {
int read = file.read(buffer.array(), pos, remaining);
if (read == -1) {
System.arraycopy(PageFile.CLEAN_DATA, 0, buffer.array(), pos, remaining);
break;
}
remaining -= read;
pos += read;
}
lastPageNumber = pageNumber;
return buffer;
}
static final String transaction_log_file_extension = ".t";
public DataOutputStream openTransactionLog() throws IOException {
String logName = fileName + transaction_log_file_extension;
final FileOutputStream fileOut = new FileOutputStream(logName);
return new DataOutputStream(new BufferedOutputStream(fileOut)) {
//default implementation of flush on FileOutputStream does nothing,
//so we use little workaround to make sure that data were really flushed
public void flush() throws IOException {
super.flush();
fileOut.flush();
fileOut.getFD().sync();
}
};
}
public void deleteAllFiles() {
deleteTransactionLog();
StorageDiskMapped.deleteFiles(fileName);
}
/**
* Synchronizes the file.
*/
public void sync() throws IOException {
for (RandomAccessFile file : rafs)
if (file != null)
file.getFD().sync();
for (RandomAccessFile file : rafsTranslation)
if (file != null)
file.getFD().sync();
}
public void forceClose() throws IOException {
for (RandomAccessFile f : rafs) {
if (f != null)
f.close();
}
rafs = null;
for (RandomAccessFile f : rafsTranslation) {
if (f != null)
f.close();
}
rafsTranslation = null;
}
public DataInputStream readTransactionLog() {
File logFile = new File(fileName + transaction_log_file_extension);
if (!logFile.exists())
return null;
if (logFile.length() == 0) {
logFile.delete();
return null;
}
DataInputStream ois = null;
try {
ois = new DataInputStream(new BufferedInputStream(new FileInputStream(logFile)));
} catch (FileNotFoundException e) {
//file should exists, we check for its presents just a miliseconds yearlier, anyway move on
return null;
}
try {
if (ois.readShort() != Magic.LOGFILE_HEADER)
throw new Error("Bad magic on log file");
} catch (IOException e) {
// corrupted/empty logfile
logFile.delete();
return null;
}
return ois;
}
public void deleteTransactionLog() {
File logFile = new File(fileName + transaction_log_file_extension);
if (logFile.exists())
logFile.delete();
}
public boolean isReadonly() {
return false;
}
}

View File

@ -0,0 +1,259 @@
package org.apache.jdbm;
import sun.misc.Cleaner;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
/**
* Disk storage which uses mapped buffers
*/
class StorageDiskMapped implements Storage {
static final String IDR = ".i";
static final String DBR = ".d";
/**
* Maximal number of pages in single file.
* Calculated so that each file will have 1 GB
*/
final static long PAGES_PER_FILE = (1024*1024*1024)>>>Storage.PAGE_SIZE_SHIFT;
private ArrayList<FileChannel> channels = new ArrayList<FileChannel>();
private ArrayList<FileChannel> channelsTranslation = new ArrayList<FileChannel>();
private IdentityHashMap<FileChannel, MappedByteBuffer> buffers = new IdentityHashMap<FileChannel, MappedByteBuffer>();
private String fileName;
private boolean transactionsDisabled;
private boolean readonly;
private boolean lockingDisabled;
public StorageDiskMapped(String fileName, boolean readonly, boolean transactionsDisabled, boolean lockingDisabled) throws IOException {
this.fileName = fileName;
this.transactionsDisabled = transactionsDisabled;
this.readonly = readonly;
this.lockingDisabled = lockingDisabled;
//make sure first file can be opened
//lock it
try {
if(!lockingDisabled)
getChannel(0).lock();
} catch (IOException e) {
throw new IOException("Could not lock DB file: " + fileName, e);
} catch (OverlappingFileLockException e) {
throw new IOException("Could not lock DB file: " + fileName, e);
}
}
private FileChannel getChannel(long pageNumber) throws IOException {
int fileNumber = (int) (Math.abs(pageNumber)/PAGES_PER_FILE );
List<FileChannel> c = pageNumber>=0 ? channels : channelsTranslation;
//increase capacity of array lists if needed
for (int i = c.size(); i <= fileNumber; i++) {
c.add(null);
}
FileChannel ret = c.get(fileNumber);
if (ret == null) {
String name = makeFileName(fileName, pageNumber, fileNumber);
ret = new RandomAccessFile(name, "rw").getChannel();
c.set(fileNumber, ret);
buffers.put(ret, ret.map(FileChannel.MapMode.READ_WRITE, 0, ret.size()));
}
return ret;
}
static String makeFileName(String fileName, long pageNumber, int fileNumber) {
return fileName + (pageNumber>=0 ? DBR : IDR) + "." + fileNumber;
}
public void write(long pageNumber, ByteBuffer data) throws IOException {
if(transactionsDisabled && data.isDirect()){
//if transactions are disabled and this buffer is direct,
//changes written into buffer are directly reflected in file.
//so there is no need to write buffer second time
return;
}
FileChannel f = getChannel(pageNumber);
int offsetInFile = (int) ((Math.abs(pageNumber) % PAGES_PER_FILE)* PAGE_SIZE);
MappedByteBuffer b = buffers.get(f);
if( b.limit()<=offsetInFile){
//remapping buffer for each newly added page would be slow,
//so allocate new size in chunks
int increment = Math.min(PAGE_SIZE * 1024,offsetInFile/10);
increment -= increment% PAGE_SIZE;
long newFileSize = offsetInFile+ PAGE_SIZE + increment;
newFileSize = Math.min(PAGES_PER_FILE * PAGE_SIZE, newFileSize);
//expand file size
f.position(newFileSize - 1);
f.write(ByteBuffer.allocate(1));
//unmap old buffer
unmapBuffer(b);
//remap buffer
b = f.map(FileChannel.MapMode.READ_WRITE, 0,newFileSize);
buffers.put(f, b);
}
//write into buffer
b.position(offsetInFile);
data.rewind();
b.put(data);
}
private void unmapBuffer(MappedByteBuffer b) {
if(b!=null){
Cleaner cleaner = ((sun.nio.ch.DirectBuffer) b).cleaner();
if(cleaner!=null)
cleaner.clean();
}
}
public ByteBuffer read(long pageNumber) throws IOException {
FileChannel f = getChannel(pageNumber);
int offsetInFile = (int) ((Math.abs(pageNumber) % PAGES_PER_FILE)* PAGE_SIZE);
MappedByteBuffer b = buffers.get(f);
if(b == null){ //not mapped yet
b = f.map(FileChannel.MapMode.READ_WRITE, 0, f.size());
}
//check buffers size
if(b.limit()<=offsetInFile){
//file is smaller, return empty data
return ByteBuffer.wrap(PageFile.CLEAN_DATA).asReadOnlyBuffer();
}
b.position(offsetInFile);
ByteBuffer ret = b.slice();
ret.limit(PAGE_SIZE);
if(!transactionsDisabled||readonly){
// changes written into buffer will be directly written into file
// so we need to protect buffer from modifications
ret = ret.asReadOnlyBuffer();
}
return ret;
}
public void forceClose() throws IOException {
for(FileChannel f: channels){
if(f==null) continue;
f.close();
unmapBuffer(buffers.get(f));
}
for(FileChannel f: channelsTranslation){
if(f==null) continue;
f.close();
unmapBuffer(buffers.get(f));
}
channels = null;
channelsTranslation = null;
buffers = null;
}
public void sync() throws IOException {
for(MappedByteBuffer b: buffers.values()){
b.force();
}
}
public DataOutputStream openTransactionLog() throws IOException {
String logName = fileName + StorageDisk.transaction_log_file_extension;
final FileOutputStream fileOut = new FileOutputStream(logName);
return new DataOutputStream(new BufferedOutputStream(fileOut)) {
//default implementation of flush on FileOutputStream does nothing,
//so we use little workaround to make sure that data were really flushed
public void flush() throws IOException {
super.flush();
fileOut.flush();
fileOut.getFD().sync();
}
};
}
public void deleteAllFiles() throws IOException {
deleteTransactionLog();
deleteFiles(fileName);
}
static void deleteFiles(String fileName) {
for(int i = 0; true; i++){
String name = makeFileName(fileName,+1, i);
File f =new File(name);
boolean exists = f.exists();
if(exists && !f.delete()) f.deleteOnExit();
if(!exists) break;
}
for(int i = 0; true; i++){
String name = makeFileName(fileName,-1, i);
File f =new File(name);
boolean exists = f.exists();
if(exists && !f.delete()) f.deleteOnExit();
if(!exists) break;
}
}
public DataInputStream readTransactionLog() {
File logFile = new File(fileName + StorageDisk.transaction_log_file_extension);
if (!logFile.exists())
return null;
if (logFile.length() == 0) {
logFile.delete();
return null;
}
DataInputStream ois = null;
try {
ois = new DataInputStream(new BufferedInputStream(new FileInputStream(logFile)));
} catch (FileNotFoundException e) {
//file should exists, we check for its presents just a miliseconds yearlier, anyway move on
return null;
}
try {
if (ois.readShort() != Magic.LOGFILE_HEADER)
throw new Error("Bad magic on log file");
} catch (IOException e) {
// corrupted/empty logfile
logFile.delete();
return null;
}
return ois;
}
public void deleteTransactionLog() {
File logFile = new File(fileName + StorageDisk.transaction_log_file_extension);
if (logFile.exists())
logFile.delete();
}
public boolean isReadonly() {
return readonly;
}
}

View File

@ -0,0 +1,96 @@
package org.apache.jdbm;
import java.io.*;
import java.nio.ByteBuffer;
/**
* Storage which keeps all data in memory.
* Data are lost after storage is closed.
*/
class StorageMemory implements Storage {
private LongHashMap<byte[]> pages = new LongHashMap<byte[]>();
private boolean transactionsDisabled;
StorageMemory(boolean transactionsDisabled){
this.transactionsDisabled = transactionsDisabled;
}
public ByteBuffer read(long pageNumber) throws IOException {
byte[] data = pages.get(pageNumber);
if (data == null) {
//out of bounds, so just return empty data
return ByteBuffer.wrap(PageFile.CLEAN_DATA).asReadOnlyBuffer();
}else{
ByteBuffer b = ByteBuffer.wrap(data);
if(!transactionsDisabled)
return b.asReadOnlyBuffer();
else
return b;
}
}
public void write(long pageNumber, ByteBuffer data) throws IOException {
if (data.capacity() != PAGE_SIZE) throw new IllegalArgumentException();
byte[] b = pages.get(pageNumber);
if(transactionsDisabled && data.hasArray() && data.array() == b){
//already putted directly into array
return;
}
if(b == null)
b = new byte[PAGE_SIZE];
data.position(0);
data.get(b,0, PAGE_SIZE);
pages.put(pageNumber,b);
}
public void sync() throws IOException {
}
public void forceClose() throws IOException {
pages = null;
}
private ByteArrayOutputStream transLog;
public DataInputStream readTransactionLog() {
if (transLog == null)
return null;
DataInputStream ret = new DataInputStream(
new ByteArrayInputStream(transLog.toByteArray()));
//read stream header
try {
ret.readShort();
} catch (IOException e) {
throw new IOError(e);
}
return ret;
}
public void deleteTransactionLog() {
transLog = null;
}
public DataOutputStream openTransactionLog() throws IOException {
if (transLog == null)
transLog = new ByteArrayOutputStream();
return new DataOutputStream(transLog);
}
public void deleteAllFiles() throws IOException {
}
public boolean isReadonly() {
return false;
}
}

View File

@ -0,0 +1,71 @@
package org.apache.jdbm;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* A read-only storage which reads data from compressed zip archive.
* <p/>
* To improve performance with compressed archives
* each page is stored in separate file (zip archive entry).
*/
class StorageZip implements Storage {
private String zip;
private String zip2;
private ZipFile z;
StorageZip(String zipFile) throws IOException {
zip = zipFile;
z = new ZipFile(zip);
zip2 = "db";
}
public void write(long pageNumber, ByteBuffer data) throws IOException {
throw new UnsupportedOperationException("readonly");
}
public ByteBuffer read(long pageNumber) throws IOException {
ByteBuffer data = ByteBuffer.allocate(PAGE_SIZE);
ZipEntry e = z.getEntry(zip2 + pageNumber);
if(e == null)
return ByteBuffer.wrap(PageFile.CLEAN_DATA).asReadOnlyBuffer();
InputStream i = z.getInputStream(e);
new DataInputStream(i).readFully(data.array());
i.close();
return data;
}
public void forceClose() throws IOException {
z.close();
z = null;
}
public DataInputStream readTransactionLog() {
throw new UnsupportedOperationException("readonly");
}
public void deleteTransactionLog() {
throw new UnsupportedOperationException("readonly");
}
public void sync() throws IOException {
throw new UnsupportedOperationException("readonly");
}
public DataOutputStream openTransactionLog() throws IOException {
throw new UnsupportedOperationException("readonly");
}
public void deleteAllFiles() throws IOException {
}
public boolean isReadonly() {
return true;
}
}

View File

@ -0,0 +1,106 @@
package org.apache.jdbm;
import javax.crypto.Cipher;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOError;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Comparator;
/**
* Various utilities used in JDBM
*/
class Utils {
/**
* empty string is used as dummy value to represent null values in HashSet and TreeSet
*/
static final String EMPTY_STRING = "";
public static byte[] encrypt(Cipher cipherIn, ByteBuffer b) {
if(cipherIn==null && b.hasArray())
return b.array();
byte[] bb = new byte[Storage.PAGE_SIZE];
b.rewind();
b.get(bb,0,Storage.PAGE_SIZE);
return encrypt(cipherIn,bb);
}
public static byte[] encrypt(Cipher cipherIn, byte[] b) {
if (cipherIn == null)
return b;
try {
return cipherIn.doFinal(b);
} catch (Exception e) {
throw new IOError(e);
}
}
/**
* Compares comparables. Default comparator for most of java types
*/
static final Comparator COMPARABLE_COMPARATOR = new Comparator<Comparable>() {
public int compare(Comparable o1, Comparable o2) {
return o1 == null && o2 != null ? -1 : (o1 != null && o2 == null ? 1 : o1.compareTo(o2));
}
};
static String formatSpaceUsage(long size) {
if (size < 1e4)
return size + "B";
else if (size < 1e7)
return "" + Math.round(1D * size / 1024D) + "KB";
else if (size < 1e10)
return "" + Math.round(1D * size / 1e6) + "MB";
else
return "" + Math.round(1D * size / 1e9) + "GB";
}
static boolean allZeros(byte[] b) {
for (int i = 0; i < b.length; i++) {
if (b[i] != 0) return false;
}
return true;
}
static <E> E max(E e1, E e2, Comparator comp){
if(e1 == null) return e2;
if(e2 == null) return e1;
if(comp == null)
comp = COMPARABLE_COMPARATOR;
return comp.compare(e1,e2)<0 ? e2:e1;
}
static <E> E min(E e1, E e2, Comparator comp){
if(e1 == null) return e2;
if(e2 == null) return e1;
if(comp == null)
comp = COMPARABLE_COMPARATOR;
return comp.compare(e1,e2)>0 ? e2:e1;
}
static final Serializer<Object> NULL_SERIALIZER = new Serializer<Object>() {
public void serialize(DataOutput out, Object obj) throws IOException {
out.writeByte(11);
}
public Object deserialize(DataInput in) throws IOException, ClassNotFoundException {
in.readByte();
return null;
}
};
}

View File

@ -0,0 +1,200 @@
<html>
<body>
<h1>WARNING incomplete and missleading doc!!!</h1>
<p>This package contains public API and introduction</p>
<h2>JDBM intro</h2>
Key-Value databases have got a lot of attention recently, but their history is much older. GDBM (predecessor of JDBM)
started
in 1970 and was called 'pre rational' database. JDBM is under development since 2000. Version 1.0 was in production
since 2005 with only a few bugs reported. Version 2.0 adds some features on top of JDBM (most importantly <code>java.util.Map</code>
views)
<p/>
JDBM 2.0 goal is to provide simple and fast persistence. It is very simple to use, it has minimal overhead and
standalone
JAR takes only 130KB. It is excelent choice for Swing application or Android phone. JDBM also handles huge datasets well
and can be used for data processing (author is using it to process astronomical data).
The source code is not complicated; it is well readabable and can also be used for teaching.
On the other hand, it does not have some important features (concurrent scalability, multiple transaction, annotations,
clustering...), which is the reason why it is so simple and small. For example, multiple transaction would introduce a
new dimension of problems, such as concurrent updates, optimistic/pesimistic record locking, etc.
JDBM does not try to replicate Valdemort, HBase or other more advanced Key Value databases.
<p/>
<h2>JDBM2 is </h2>
<p/><b>Not a SQL database</b><br/>
JDBM2 is more low level. With this comes great power (speed, resource usage, no ORM)
but also big responsibility. You are responsible for data integrity, partioning, typing etc...
Excelent embedded SQL database is <a href="http://www.h2database.com">H2</a> (in fact it is faster than JDBM2 in many
cases).
<p/><b>Not an Object database</b><br/>
The fact that JDBM2 uses serialization may give you a false sense of security. It does not
magically split a huge object graph into smaller pieces, nor does it handle duplicates.
With JDBM you may easily end up with single instance being persisted in several copies over a datastore.
An object database would do this magic for you as it traverses object graph references and
makes sure there are no duplicates in a datastore. Have look at
<a href="http://www.neodatis.org/">NeoDatis</a> or <a href="http://www.db4o.com/">DB4o</a>
<p/><b>Not at enterprise level</b><br/>
JDBM2 codebase is propably very good and without bugs, but it is a community project. You may easily endup without
support. For something more enterprisey have a look at
<a href="http://www.oracle.com/database/berkeley-db/je/index.html ">Berkley DB Java Edition</a> from Oracle. BDB has
more
features, it is more robust, it has better documentation, bigger overhead and comes with a pricetag.
<p/><b>Not distributed</b><br/>
Key Value databases are associated with distributed stores, map reduce etc. JDBM is not distributed, it runs on single
computer only.
It does not even have a network interface and can not act as a server.
You would be propably looking for <a href="http://project-voldemort.com/">Valdemort</a>.
<h2>JDBM2 overview</h2>
JDBM2 has some helpfull features to make it easier to use. It also brings it closer to SQL and helps with data
integrity checks and data queries.
<p/><b>Low level node store</b><br/>
This is Key-Value database in its literal mean. Key is a record identifier number (recid) which points to a location in
file.
Since recid is a physical pointer, new key values must be assgned by store (wherever the free space is found).
Value can be any object, serializable to a byte[] array. Page store also provides transaction and cache.
<p/><b>Named objects</b><br/>
Number as an identifier is not very practical. So there is a table that translates Strings to recid. This is recommended
approach for persisting singletons.
<p/><b>Primary maps</b><br/>
{@link jdbm.PrimaryTreeMap} and {@link jdbm.PrimaryHashMap} implements <code>java.util.map</code> interface
from Java Collections. But they use node store for persistence. So you can create HashMap with bilions of items and
worry only about the commits.
<p/><b>Secondary maps</b><br/>
Secondary maps (indexes) provide side information and associations for the primary map. For example, if there is a
Person class persisted in the primary map,
the secondary maps can provide fast lookup by name, address, age... The secondary maps are 'views' to the primary map
and are readonly.
They are updated by the primary map automatically.
<p/><b>Cache</b><br/>
JDBM has object instance cache. This reduces the serialization time and disk IO. By default JDBM uses SoftReference
cache. If JVM have
less then 50MB heap space available, MRU (Most Recently Used) fixed size cache is used instead.
<p/><b>Transactions</b><br/>
JDBM provides transactions with commit and rollback. The transaction mechanism is safe and tested (in usage for the last
5 years). JDBM allows only
single concurrent transactions and there are no problems with concurrent updates and locking.
<h1>10 things to keep in mind</h1>
<ul>
<li>Uncommited data are stored in memory, and if you get <code>OutOfMemoryException</code> you have to make commits
more
frequently.
<li>Keys and values are stored as part of the index nodes. They are instanciated each time the index is searched.
If you have larger values (>512 bytes), these may hurt performance and cause <code>OutOfMemoryException</code>
<li>If you run into performance problems, use the profiler rather then asking for it over the internet.
<li>JDBM caches returned object instances. If you modify an object (like set new name on a person),
next time RecordManager may return the object with this modification.
<li>Iteration over Maps is not guaranteed if there are changes
(for example adding a new entry while iterating). There is no fail fast policy yet.
So all iterations over Maps should be synchronized on RecordManager.
<li>More memory means better performance; use <code>-Xmx000m</code> generously. JDBM has good SoftReference cache.
<li>SoftReference cache may be blocking some memory for other tasks. The memory is released automatically, but it
may take longer then you expect.
Consider clearing the cache manually with <code>RecordManager.clearCache()</code> before starting a new type
of task.
<li>It is safe not to close the db before exiting, but if you that there will be a long cleanup upon the next start.
<li>JDBM may have problem reclaiming free space after many records are delete/updated. You may want to run
<code>RecordManager.defrag()</code> from time to time.
<li>A Key-Value db does not support N-M relations easily. It takes a lot of care to handle them correctly.
</ul>
<dl>
</dl>
<!-- $Id: package.html,v 1.1 2001/05/19 16:01:33 boisvert Exp $ -->
<html>
<body>
<p>Core classes for managing persistent objects and processing transactions.</p>
<h1>Memory allocation</h1>
This document describes the memory allocation structures and
algorithms used by jdbm. It is based on a thread in the
jdbm-developers mailing list.
<p/>
<ul>
<li> A block is a fixed length of bytes. Also known as a node.
<li> A row is a variable length of bytes. Also known as a record.
<li> A slot is a fixed length entry in a given block/node.
<li> A node list is a linked list of pages. The head and tail of each
node list is maintained in the file header.
</ul>
Jdbm knows about a few node lists which are pre-defined in Magic,
e.g., Magic.USED_PAGE. The FREE, USED, TRANSLATION, FREELOGIDS, and
FREEPHYSIDS node lists are used by the jdbm memory allocation policy
and are described below.
<p/>
The translation list consists of a bunch of slots that can be
available (free) or unavailable (allocated). If a slot is available,
then it contains junk data (it is available to map the logical row id
associated with that slot to some physical row id). If it is
unavailable, then it contains the block id and offset of the header of
a valid (non-deleted) record. "Available" for the translation list
is marked by a zero block id for that slot.
<p/>
The free logical row id list consists of a set of pages that contain
slots. Each slot is either available (free) or unavailable
(allocated). If it is unavailable, then it contains a reference to
the location of the available slot in the translation list. If it is
available, then it contains junk data. "Available" slots are marked by
a zero block id. A count is maintained of the #of available slots
(free row ids) on the node.
<p/>
As you free a logical row id, you change it's slot in the translation
list from unavailable to available, and then *add* entries to the free
logical row list. Adding entries to the free logical row list is done
by finding an available slot in the free logical row list and
replacing the junk data in that slot with the location of the now
available slot in the translation list. A count is maintained of the
#of available slots (free row ids) on the node.
<p/>
Whew... now we've freed a logical row id. But what about the physical
row id?
<p/>
Well, the free physical row id list consists of a set of pages that
contain slots. Each slot is either available (free) or unavailable
(allocated). If it is unavailable, then it contains a reference to
the location of the newly freed row's header in the data node. If it
is available, then it contains junk data. "Available" slots are
marked by a zero block id. A count is maintained of the #of available
slots (free row ids) on the node. (Sound familiar?)
<p/>
As you free a physical row id, you change it's header in the data node
from inuse to free (by zeroing the size field of the record header),
and then *add* an entry to the free physical row list. Adding entries
to the free physical row list consists of finding an available slot,
and replacing the junk data in that slot with the location of the
newly freed row's header in the data node.
<p/>
The translation list is used for translating in-use logical row ids
to in-use physical row ids. When a physical row id is freed, it is
removed from the translation list and added to the free physical row
id list.
<p/>
This allows a complete decoupling of the logical row id from the
physical row id, which makes it super easy to do some of the fiddling
I'm talking about the coallescing and splitting records.
<p/>
If you want to get a list of the free records, just enumerate the
unavailable entries in the free physical row id list. You don't even
need to look up the record header because the length of the record is
also stored in the free physical row id list. As you enumerate the
list, be sure to not include slots that are available (in the current
incarnation of jdbm, I believe the available length is set to 0 to
indicate available - we'll be changing that some time soon here, I'm
sure).
<p/>
</body>
</html>
</body>
</html>

View File

@ -16,7 +16,7 @@
<stringAttribute key="location" value="${workspace_loc}/../runtime-com.minres.scviewer.e4.application.product"/>
<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/>
<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -consoleLog -clearPersistedState -data @none"/>
<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -consoleLog -clearPersistedState -data @none /Users/eyck/Workspaces/SCViewer_eyck/atmega.txlog"/>
<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.pde.ui.workbenchClasspathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xms40m -Xmx4G -Xdock:icon=../Resources/Eclipse.icns -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts"/>
<stringAttribute key="pde.version" value="3.3"/>