/*
* JCaptcha, the open source java framework for captcha definition and integration
* Copyright (c) 2007 jcaptcha.net. All Rights Reserved.
* See the LICENSE.txt file distributed with this package.
*/
/*
* jcaptcha, the open source java framework for captcha definition and integration
* copyright (c) 2007 jcaptcha.net. All Rights Reserved.
* See the LICENSE.txt file distributed with this package.
*/
/*
* jcaptcha, the open source java framework for captcha definition and integration
* copyright (c) 2007 jcaptcha.net. All Rights Reserved.
* See the LICENSE.txt file distributed with this package.
*/
package com.octo.captcha.engine.bufferedengine.buffer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.io.StreamCorruptedException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.NoSuchElementException;
import org.apache.commons.collections.MapIterator;
import org.apache.commons.collections.buffer.UnboundedFifoBuffer;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.octo.captcha.Captcha;
import com.octo.captcha.CaptchaException;
/**
* Simple implmentation of a disk captcha buffer
*
* @author Benoit Doumas
*/
public class DiskCaptchaBuffer implements CaptchaBuffer {
private static final Log log = LogFactory.getLog(DiskCaptchaBuffer.class.getName());
private RandomAccessFile randomAccessFile;
private HashedMap diskElements = null;
private ArrayList freeSpace;
/**
* If persistent, the disk file will be kept and reused on next startup. In addition the memory store will flush all
* contents to spool, and spool will flush all to disk.
*/
private boolean persistant = false;
private final String name;
private File dataFile;
/**
* Used to persist elements
*/
private File indexFile;
private boolean isInitalized = false;
/**
* The size in bytes of the disk elements
*/
private long totalSize;
/**
* The max size in Kbytes of the disk elements
*/
private int maxDataSize;
private boolean isDisposed = false;
/**
* Constructor for a disk captcha buffer
*
* @param fileName like c:/temp/name
* @param persistant If the disk buffer is persistant, it will try to load from file name .data et .index existing
* data
*/
public DiskCaptchaBuffer(String fileName, boolean persistant) {
log.debug("Creating new Diskbuffer");
freeSpace = new ArrayList();
this.name = fileName;
this.persistant = persistant;
try {
initialiseFiles();
}
catch (Exception e) {
log.debug("Error while initialising files " + e);
}
}
private final void initialiseFiles() throws Exception {
dataFile = new File(name + ".data");
indexFile = new File(name + ".index");
readIndex();
if (diskElements == null || !persistant) {
if (log.isDebugEnabled()) {
log.debug("Index file dirty or empty. Deleting data file " + getDataFileName());
}
dataFile.delete();
diskElements = new HashedMap();
}
// Open the data file as random access. The dataFile is created if necessary.
randomAccessFile = new RandomAccessFile(dataFile, "rw");
isInitalized = true;
log.info("Buffer initialized");
}
/**
* Gets an entry from the Disk Store.
*
* @return The element
*/
protected synchronized Collection remove(int number, Locale locale) throws IOException {
if (!isInitalized) return new ArrayList(0);
DiskElement diskElement = null;
int index = 0;
boolean diskEmpty = false;
Collection collection = new UnboundedFifoBuffer();
//if no locale
if (!diskElements.containsKey(locale)) {
return collection;
}
try {
while (!diskEmpty && index < number) {
// Check if the element is on disk
try {
diskElement = (DiskElement) ((LinkedList) diskElements.get(locale))
.removeFirst();
// Load the element
randomAccessFile.seek(diskElement.position);
byte[] buffer = new byte[diskElement.payloadSize];
randomAccessFile.readFully(buffer);
ByteArrayInputStream instr = new ByteArrayInputStream(buffer);
ObjectInputStream objstr = new ObjectInputStream(instr);
collection.add(objstr.readObject());
instr.close();
objstr.close();
freeBlock(diskElement);
index++;
}
catch (NoSuchElementException e) {
diskEmpty = true;
log.debug("disk is empty for locale : " + locale.toString());
}
}
}
catch (Exception e) {
log.error("Error while reading on disk ", e);
}
if (log.isDebugEnabled()) {
log.debug("removed " + collection.size() + " from disk buffer with locale "
+ locale.toString());
}
return collection;
}
/**
* Puts items into the store.
*/
protected synchronized void store(Collection collection, Locale locale) throws IOException {
if (!isInitalized) return;
// Write elements to the DB
for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
final Object element = iterator.next();
// Serialise the entry
final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
final ObjectOutputStream objstr = new ObjectOutputStream(outstr);
objstr.writeObject(element);
objstr.close();
//check if there is space
store(element, locale);
}
}
/**
* Puts items into the store.
*/
protected synchronized void store(Object element, Locale locale) throws IOException {
if (!isInitalized) return;
// Serialise the entry
final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
final ObjectOutputStream objstr = new ObjectOutputStream(outstr);
objstr.writeObject(element);
objstr.close();
final byte[] buffer = outstr.toByteArray();
//check if there is space
// if (diskElements.size() >= maxDataSize)
// {
// return false;
// }
// Check for a free block
DiskElement diskElement = findFreeBlock(buffer.length);
if (diskElement == null) {
diskElement = new DiskElement();
diskElement.position = randomAccessFile.length();
diskElement.blockSize = buffer.length;
}
// TODO - cleanup block on failure
// Write the record
randomAccessFile.seek(diskElement.position);
//TODO the free block algorithm will gradually leak disk space, due to
//payload size being less than block size
//this will be a problem for the persistent cache
randomAccessFile.write(buffer);
// Add to index, update stats
diskElement.payloadSize = buffer.length;
totalSize += buffer.length;
//create the localized buffer
if (!diskElements.containsKey(locale)) {
diskElements.put(locale, new LinkedList());
}
((LinkedList) diskElements.get(locale)).addLast(diskElement);
if (log.isDebugEnabled()) {
long menUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
log.debug("Store " + locale.toString() + " on object, total size : " + size()
+ " Total unsed elements : " + freeSpace.size() + " memory used " + menUsed);
}
}
/**
* Marks a block as free.
*/
private void freeBlock(final DiskElement element) {
totalSize -= element.payloadSize;
element.payloadSize = 0;
freeSpace.add(element);
}
/**
* Removes all cached items from the cache. <p/>
*/
public synchronized void clearFile() throws IOException {
try {
// Ditch all the elements, and truncate the file
diskElements.clear();
freeSpace.clear();
totalSize = 0;
randomAccessFile.setLength(0);
indexFile.delete();
indexFile.createNewFile();
}
catch (Exception e) {
// Clean up
log.error(" Cache: Could not rebuild disk store", e);
dispose();
}
}
/**
* Shuts down the disk store in preparation for cache shutdown <p/>If a VM crash happens, the shutdown hook will not
* run. The data file and the index file will be out of synchronisation. At initialisation we always delete the
* index file after we have read the elements, so that it has a zero length. On a dirty restart, it still will have
* and the data file will automatically be deleted, thus preserving safety.
*/
public synchronized void dispose() {
//set allready in case some concurrent access
isDisposed = true;
// Close the cache
try {
//Flush the spool if persistent, so we don't lose any data.
writeIndex();
//Clear in-memory data structures
diskElements.clear();
freeSpace.clear();
if (randomAccessFile != null) {
randomAccessFile.close();
}
}
catch (Exception e) {
log.error("Cache: Could not shut down disk cache", e);
}
finally {
randomAccessFile = null;
}
}
/**
* Writes the Index to disk on shutdown <p/>The index consists of the elements Map and the freeSpace List <p/>Note
* that the cache is locked for the entire time that the index is being written
*/
private synchronized void writeIndex() throws IOException {
ObjectOutputStream objectOutputStream = null;
try {
FileOutputStream fout = new FileOutputStream(indexFile);
objectOutputStream = new ObjectOutputStream(fout);
objectOutputStream.writeObject(diskElements);
objectOutputStream.writeObject(freeSpace);
}
finally {
if (objectOutputStream != null) {
objectOutputStream.flush();
objectOutputStream.close();
}
}
}
/**
* Reads Index to disk on startup. <p/>if the index file does not exist, it creates a new one. <p/>Note that the
* cache is locked for the entire time that the index is being written
*/
private synchronized void readIndex() throws IOException {
ObjectInputStream objectInputStream = null;
FileInputStream fin = null;
if (indexFile.exists() && persistant) {
try {
fin = new FileInputStream(indexFile);
objectInputStream = new ObjectInputStream(fin);
diskElements = (HashedMap) objectInputStream.readObject();
freeSpace = (ArrayList) objectInputStream.readObject();
}
catch (StreamCorruptedException e) {
log.error("Corrupt index file. Creating new index.");
createNewIndexFile();
}
catch (IOException e) {
log.error("IOException reading index. Creating new index. ");
createNewIndexFile();
}
catch (ClassNotFoundException e) {
log.error("Class loading problem reading index. Creating new index. ", e);
createNewIndexFile();
}
finally {
try {
if (objectInputStream != null) {
objectInputStream.close();
} else if (fin != null) {
fin.close();
}
}
catch (IOException e) {
log.error("Problem closing the index file.");
}
}
} else {
createNewIndexFile();
}
}
private void createNewIndexFile() throws IOException {
if (indexFile.exists()) {
indexFile.delete();
if (log.isDebugEnabled()) {
log.debug("Index file " + indexFile + " deleted.");
}
}
if (indexFile.createNewFile()) {
if (log.isDebugEnabled()) {
log.debug("Index file " + indexFile + " created successfully");
}
} else {
throw new IOException("Index file " + indexFile + " could not created.");
}
}
/**
* Allocates a free block.
*/
private DiskElement findFreeBlock(final int length) {
for (int i = 0; i < freeSpace.size(); i++) {
final DiskElement element = (DiskElement) freeSpace.get(i);
if (element.blockSize >= length) {
freeSpace.remove(i);
return element;
}
}
return null;
}
/**
* Returns a {@link String}representation of the {@link DiskCaptchaBuffer}
*/
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("[ dataFile = ").append(dataFile.getAbsolutePath()).append(", totalSize=")
.append(totalSize).append(", status=").append(isInitalized).append(" ]");
return sb.toString();
}
/**
* A reference to an on-disk elements.
*/
private static class DiskElement implements Serializable {
/**
* the file pointer
*/
private long position;
/**
* The size used for data.
*/
private int payloadSize;
/**
* the size of this element.
*/
private int blockSize;
}
/**
* @return the total size of the data file and the index file, in bytes.
*/
public long getTotalFileSize() {
return getDataFileSize() + getIndexFileSize();
}
/**
* @return the size of the data file in bytes.
*/
public long getDataFileSize() {
return dataFile.length();
}
/**
* The design of the layout on the data file means that there will be small gaps created when DiskElements are
* reused.
*
* @return the sparseness, measured as the percentage of space in the Data File not used for holding data
*/
public float calculateDataFileSparseness() {
return 1 - ((float) getUsedDataSize() / (float) getDataFileSize());
}
/**
* When elements are deleted, spaces are left in the file. These spaces are tracked and are reused when new elements
* need to be written. <p/>This method indicates the actual size used for data, excluding holes. It can be compared
* with {@link #getDataFileSize()}as a measure of fragmentation.
*/
public long getUsedDataSize() {
return totalSize;
}
/**
* @return the size of the index file, in bytes.
*/
public long getIndexFileSize() {
if (indexFile == null) {
return 0;
} else {
return indexFile.length();
}
}
/**
* @return the file name of the data file where the disk store stores data, without any path information.
*/
public String getDataFileName() {
return name + ".data";
}
/**
* @return the file name of the index file, which maintains a record of elements and their addresses on the data
* file, without any path information.
*/
public String getIndexFileName() {
return name + ".index";
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#removeCaptcha()
*/
public Captcha removeCaptcha() throws NoSuchElementException {
if (isDisposed) return null;
return removeCaptcha(Locale.getDefault());
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#removeCaptcha(int)
*/
public Collection removeCaptcha(int number) {
if (isDisposed) return null;
log.debug("Entering removeCaptcha(int number) ");
Collection c = null;
try {
c = remove(number, Locale.getDefault());
}
catch (IOException e) {
throw new CaptchaException(e);
}
return c;
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#putCaptcha(com.octo.captcha.Captcha)
*/
public void putCaptcha(Captcha captcha) {
log.debug("Entering putCaptcha(Captcha captcha)");
putCaptcha(captcha, Locale.getDefault());
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#putAllCaptcha(java.util.Collection)
*/
public void putAllCaptcha(Collection captchas) {
log.debug("Entering putAllCaptcha()");
putAllCaptcha(captchas, Locale.getDefault());
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#size()
*/
public int size() {
if (!isInitalized) return 0;
int total = 0;
MapIterator it = diskElements.mapIterator();
while (it.hasNext()) {
it.next();
total += ((LinkedList) it.getValue()).size();
}
return total;
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#maxSize()
*/
public int maxSize() {
return (int) this.maxDataSize;
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#removeCaptcha(java.util.Locale)
*/
public Captcha removeCaptcha(Locale locale) throws NoSuchElementException {
log.debug("entering removeCaptcha(Locale locale)");
Collection captchas = null;
try {
captchas = remove(1, locale);
}
catch (IOException e) {
throw new CaptchaException(e);
}
if (captchas.size() == 0) {
throw new NoSuchElementException();
}
return (Captcha) captchas.toArray()[0];
}
/**
* @see CaptchaBuffer#removeCaptcha(int, java.util.Locale)
*/
public Collection removeCaptcha(int number, Locale locale) {
if (isDisposed) return null;
try {
return remove(number, locale);
}
catch (IOException e) {
throw new CaptchaException(e);
}
}
/**
* @see CaptchaBuffer#putCaptcha(com.octo.captcha.Captcha,
* java.util.Locale)
*/
public void putCaptcha(Captcha captcha, Locale locale) {
if (isDisposed) return;
try {
store(captcha, locale);
}
catch (IOException e) {
throw new CaptchaException(e);
}
}
/**
* @see CaptchaBuffer#putAllCaptcha(java.util.Collection,
* java.util.Locale)
*/
public void putAllCaptcha(Collection captchas, Locale locale) {
if (isDisposed) return;
try {
store(captchas, locale);
log.debug("trying to store " + captchas.size());
}
catch (IOException e) {
throw new CaptchaException(e);
}
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#size(java.util.Locale)
*/
public int size(Locale locale) {
if (!isInitalized || isDisposed) return 0;
return ((LinkedList) diskElements.get(locale)).size();
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#clear()
*/
public void clear() {
try {
clearFile();
}
catch (IOException e) {
throw new CaptchaException(e);
}
}
/**
* @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#getLocales()
*/
public Collection getLocales() {
if (isDisposed) return null;
return diskElements.keySet();
}
}