com.octo.captcha.engine.bufferedengine.buffer.DiskCaptchaBuffer.java Source code

Java tutorial

Introduction

Here is the source code for com.octo.captcha.engine.bufferedengine.buffer.DiskCaptchaBuffer.java

Source

/*
 * 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();
    }

}