/*
* Copyright (c) 2009, Harald Kuhr
* 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 "TwelveMonkeys" 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 OWNER 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 com.twelvemonkeys.util;
import com.twelvemonkeys.io.FileUtil;
import java.io.*;
import java.util.*;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* PersistentMap
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PersistentMap.java,v 1.0 May 13, 2009 2:31:29 PM haraldk Exp$
*/
public class PersistentMap<K extends Serializable, V extends Serializable> extends AbstractMap<K, V>{
public static final FileFilter DIRECTORIES = new FileFilter() {
public boolean accept(File file) {
return file.isDirectory();
}
@Override
public String toString() {
return "[All folders]";
}
};
private static final String INDEX = ".index";
private final File root;
private final Map<K, UUID> index = new LinkedHashMap<K, UUID>();
private boolean mutable = true;
// Idea 2.0:
// - Create directory per hashCode
// - Create file per object in that directory
// - Name file after serialized form of key? Base64?
// - Special case for String/Integer/Long etc?
// - Or create index file in directory with serialized objects + name (uuid) of file
// TODO: Consider single index file? Or a few? In root directory instead of each directory
// Consider a RAF/FileChannel approach instead of streams - how do we discard portions of a RAF?
// - Need to keep track of used/unused parts of file, scan for gaps etc...?
// - Need to periodically truncate and re-build the index (always as startup, then at every N puts/removes?)
/*public */PersistentMap(String id) {
this(new File(FileUtil.getTempDirFile(), id));
}
public PersistentMap(File root) {
this.root = notNull(root);
init();
}
private void init() {
if (!root.exists() && !root.mkdirs()) {
throw new IllegalStateException(String.format("'%s' does not exist/could not be created", root.getAbsolutePath()));
}
else if (!root.isDirectory()) {
throw new IllegalStateException(String.format("'%s' exists but is not a directory", root.getAbsolutePath()));
}
if (!root.canRead()) {
throw new IllegalStateException(String.format("'%s' is not readable", root.getAbsolutePath()));
}
if (!root.canWrite()) {
mutable = false;
}
FileUtil.visitFiles(root, DIRECTORIES, new Visitor<File>() {
public void visit(File dir) {
// - Read .index file
// - Add entries to index
ObjectInputStream input = null;
try {
input = new ObjectInputStream(new FileInputStream(new File(dir, INDEX)));
while (true) {
@SuppressWarnings({"unchecked"})
K key = (K) input.readObject();
String fileName = (String) input.readObject();
index.put(key, UUID.fromString(fileName));
}
}
catch (EOFException eof) {
// break here
}
catch (IOException e) {
throw new RuntimeException(e);
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
finally {
FileUtil.close(input);
}
}
});
}
@Override
public Set<Entry<K, V>> entrySet() {
return new AbstractSet<Entry<K, V>>() {
@Override
public Iterator<Entry<K, V>> iterator() {
return new Iterator<Entry<K, V>>() {
Iterator<Entry<K, UUID>> indexIter = index.entrySet().iterator();
public boolean hasNext() {
return indexIter.hasNext();
}
public Entry<K, V> next() {
return new Entry<K, V>() {
final Entry<K, UUID> entry = indexIter.next();
public K getKey() {
return entry.getKey();
}
public V getValue() {
K key = entry.getKey();
int hash = key != null ? key.hashCode() : 0;
return readVal(hash, entry.getValue());
}
public V setValue(V value) {
K key = entry.getKey();
int hash = key != null ? key.hashCode() : 0;
return writeVal(key, hash, entry.getValue(), value, getValue());
}
};
}
public void remove() {
indexIter.remove();
}
};
}
@Override
public int size() {
return index.size();
}
};
}
@Override
public int size() {
return index.size();
}
@Override
public V put(K key, V value) {
V oldVal = null;
UUID uuid = index.get(key);
int hash = key != null ? key.hashCode() : 0;
if (uuid != null) {
oldVal = readVal(hash, uuid);
}
return writeVal(key, hash, uuid, value, oldVal);
}
private V writeVal(K key, int hash, UUID uuid, V value, V oldVal) {
if (!mutable) {
throw new UnsupportedOperationException();
}
File bucket = new File(root, hashToFileName(hash));
if (!bucket.exists() && !bucket.mkdirs()) {
throw new IllegalStateException(String.format("Could not create bucket '%s'", bucket));
}
if (uuid == null) {
// No uuid means new entry
uuid = UUID.randomUUID();
File idx = new File(bucket, INDEX);
ObjectOutputStream output = null;
try {
output = new ObjectOutputStream(new FileOutputStream(idx, true));
output.writeObject(key);
output.writeObject(uuid.toString());
index.put(key, uuid);
}
catch (IOException e) {
throw new RuntimeException(e);
}
finally {
FileUtil.close(output);
}
}
File entry = new File(bucket, uuid.toString());
if (value != null) {
ObjectOutputStream output = null;
try {
output = new ObjectOutputStream(new FileOutputStream(entry));
output.writeObject(value);
}
catch (IOException e) {
throw new RuntimeException(e);
}
finally {
FileUtil.close(output);
}
}
else if (entry.exists()) {
if (!entry.delete()) {
throw new IllegalStateException(String.format("'%s' could not be deleted", entry));
}
}
return oldVal;
}
private String hashToFileName(int hash) {
return Integer.toString(hash, 16);
}
@Override
public V get(Object key) {
UUID uuid = index.get(key);
if (uuid != null) {
int hash = key != null ? key.hashCode() : 0;
return readVal(hash, uuid);
}
return null;
}
private V readVal(final int hash, final UUID uuid) {
File bucket = new File(root, hashToFileName(hash));
File entry = new File(bucket, uuid.toString());
if (entry.exists()) {
ObjectInputStream input = null;
try {
input = new ObjectInputStream(new FileInputStream(entry));
//noinspection unchecked
return (V) input.readObject();
}
catch (IOException e) {
throw new RuntimeException(e);
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
finally {
FileUtil.close(input);
}
}
return null;
}
@Override
public V remove(Object key) {
// TODO!!!
return super.remove(key);
}
// TODO: Should override size, put, get, remove, containsKey and containsValue
}
/*
Memory mapped file?
Delta sync?
Persistent format
Header
File ID 4-8 bytes
Size (entries)
PersistentEntry pointer array block (PersistentEntry 0)
Size (bytes)
Next entry pointer block address (0 if last)
PersistentEntry 1 address/offset + key
...
PersistentEntry n address/offset + key
PersistentEntry 1
Size (bytes)?
Serialized value or pointer array block
...
PersistentEntry n
Size (bytes)?
Serialized value or pointer array block
*/