View Javadoc

1   /*
2    * JCaptcha, the open source java framework for captcha definition and integration
3    * Copyright (c)  2007 jcaptcha.net. All Rights Reserved.
4    * See the LICENSE.txt file distributed with this package.
5    */
6   
7   /*
8    * jcaptcha, the open source java framework for captcha definition and integration
9    * copyright (c)  2007 jcaptcha.net. All Rights Reserved.
10   * See the LICENSE.txt file distributed with this package.
11   */
12  
13  /*
14   * jcaptcha, the open source java framework for captcha definition and integration
15   * copyright (c)  2007 jcaptcha.net. All Rights Reserved.
16   * See the LICENSE.txt file distributed with this package.
17   */
18  
19  package com.octo.captcha.engine.bufferedengine.buffer;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.ObjectInputStream;
28  import java.io.ObjectOutputStream;
29  import java.io.RandomAccessFile;
30  import java.io.Serializable;
31  import java.io.StreamCorruptedException;
32  import java.util.ArrayList;
33  import java.util.Collection;
34  import java.util.Iterator;
35  import java.util.LinkedList;
36  import java.util.Locale;
37  import java.util.NoSuchElementException;
38  
39  import org.apache.commons.collections.MapIterator;
40  import org.apache.commons.collections.buffer.UnboundedFifoBuffer;
41  import org.apache.commons.collections.map.HashedMap;
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  
45  import com.octo.captcha.Captcha;
46  import com.octo.captcha.CaptchaException;
47  
48  /***
49   * Simple implmentation of a disk captcha buffer
50   *
51   * @author Benoit Doumas
52   */
53  public class DiskCaptchaBuffer implements CaptchaBuffer {
54      private static final Log log = LogFactory.getLog(DiskCaptchaBuffer.class.getName());
55  
56      private RandomAccessFile randomAccessFile;
57  
58      private HashedMap diskElements = null;
59  
60      private ArrayList freeSpace;
61  
62      /***
63       * If persistent, the disk file will be kept and reused on next startup. In addition the memory store will flush all
64       * contents to spool, and spool will flush all to disk.
65       */
66      private boolean persistant = false;
67  
68      private final String name;
69  
70      private File dataFile;
71  
72      /***
73       * Used to persist elements
74       */
75      private File indexFile;
76  
77      private boolean isInitalized = false;
78  
79      /***
80       * The size in bytes of the disk elements
81       */
82      private long totalSize;
83  
84      /***
85       * The max size in Kbytes of the disk elements
86       */
87      private int maxDataSize;
88  
89      private boolean isDisposed = false;
90  
91      /***
92       * Constructor for a disk captcha buffer
93       *
94       * @param fileName   like c:/temp/name
95       * @param persistant If the disk buffer is persistant, it will try to load from file name .data et .index existing
96       *                   data
97       */
98      public DiskCaptchaBuffer(String fileName, boolean persistant) {
99          log.debug("Creating new Diskbuffer");
100 
101         freeSpace = new ArrayList();
102         this.name = fileName;
103         this.persistant = persistant;
104 
105         try {
106             initialiseFiles();
107         }
108         catch (Exception e) {
109             log.debug("Error while initialising files " + e);
110         }
111     }
112 
113     private final void initialiseFiles() throws Exception {
114         dataFile = new File(name + ".data");
115 
116         indexFile = new File(name + ".index");
117 
118         readIndex();
119 
120         if (diskElements == null || !persistant) {
121             if (log.isDebugEnabled()) {
122                 log.debug("Index file dirty or empty. Deleting data file " + getDataFileName());
123             }
124             dataFile.delete();
125             diskElements = new HashedMap();
126         }
127 
128         // Open the data file as random access. The dataFile is created if necessary.
129         randomAccessFile = new RandomAccessFile(dataFile, "rw");
130         isInitalized = true;
131         log.info("Buffer initialized");
132     }
133 
134     /***
135      * Gets an entry from the Disk Store.
136      *
137      * @return The element
138      */
139     protected synchronized Collection remove(int number, Locale locale) throws IOException {
140         if (!isInitalized) return new ArrayList(0);
141         DiskElement diskElement = null;
142         int index = 0;
143         boolean diskEmpty = false;
144 
145         Collection collection = new UnboundedFifoBuffer();
146 
147         //if no locale
148         if (!diskElements.containsKey(locale)) {
149             return collection;
150         }
151 
152         try {
153             while (!diskEmpty && index < number) {
154 
155                 // Check if the element is on disk
156                 try {
157                     diskElement = (DiskElement) ((LinkedList) diskElements.get(locale))
158                             .removeFirst();
159 
160                     // Load the element
161                     randomAccessFile.seek(diskElement.position);
162                     byte[] buffer = new byte[diskElement.payloadSize];
163                     randomAccessFile.readFully(buffer);
164                     ByteArrayInputStream instr = new ByteArrayInputStream(buffer);
165                     ObjectInputStream objstr = new ObjectInputStream(instr);
166 
167                     collection.add(objstr.readObject());
168                     instr.close();
169                     objstr.close();
170 
171                     freeBlock(diskElement);
172                     index++;
173                 }
174                 catch (NoSuchElementException e) {
175                     diskEmpty = true;
176                     log.debug("disk is empty for locale : " + locale.toString());
177                 }
178             }
179         }
180         catch (Exception e) {
181             log.error("Error while reading on disk ", e);
182         }
183         if (log.isDebugEnabled()) {
184             log.debug("removed  " + collection.size() + " from disk buffer with locale "
185                     + locale.toString());
186         }
187         return collection;
188     }
189 
190     /***
191      * Puts items into the store.
192      */
193     protected synchronized void store(Collection collection, Locale locale) throws IOException {
194         if (!isInitalized) return;
195         // Write elements to the DB
196         for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
197             final Object element = iterator.next();
198 
199             // Serialise the entry
200             final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
201             final ObjectOutputStream objstr = new ObjectOutputStream(outstr);
202             objstr.writeObject(element);
203             objstr.close();
204 
205             //check if there is space
206             store(element, locale);
207         }
208 
209     }
210 
211     /***
212      * Puts items into the store.
213      */
214     protected synchronized void store(Object element, Locale locale) throws IOException {
215         if (!isInitalized) return;
216         // Serialise the entry
217         final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
218         final ObjectOutputStream objstr = new ObjectOutputStream(outstr);
219         objstr.writeObject(element);
220         objstr.close();
221         final byte[] buffer = outstr.toByteArray();
222 
223         //check if there is space
224         //        if (diskElements.size() >= maxDataSize)
225         //        {
226         //            return false;
227         //        }
228 
229         // Check for a free block
230         DiskElement diskElement = findFreeBlock(buffer.length);
231         if (diskElement == null) {
232             diskElement = new DiskElement();
233             diskElement.position = randomAccessFile.length();
234             diskElement.blockSize = buffer.length;
235         }
236 
237         // TODO - cleanup block on failure
238         // Write the record
239         randomAccessFile.seek(diskElement.position);
240 
241         //TODO the free block algorithm will gradually leak disk space, due to
242         //payload size being less than block size
243         //this will be a problem for the persistent cache
244         randomAccessFile.write(buffer);
245 
246         // Add to index, update stats
247         diskElement.payloadSize = buffer.length;
248         totalSize += buffer.length;
249 
250         //create the localized buffer
251         if (!diskElements.containsKey(locale)) {
252 
253             diskElements.put(locale, new LinkedList());
254         }
255         ((LinkedList) diskElements.get(locale)).addLast(diskElement);
256 
257 
258         if (log.isDebugEnabled()) {
259             long menUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
260             log.debug("Store " + locale.toString() + " on object, total size : " + size()
261                     + " Total unsed elements : " + freeSpace.size() + " memory used " + menUsed);
262         }
263     }
264 
265     /***
266      * Marks a block as free.
267      */
268     private void freeBlock(final DiskElement element) {
269         totalSize -= element.payloadSize;
270         element.payloadSize = 0;
271         freeSpace.add(element);
272     }
273 
274     /***
275      * Removes all cached items from the cache. <p/>
276      */
277     public synchronized void clearFile() throws IOException {
278         try {
279 
280             // Ditch all the elements, and truncate the file
281             diskElements.clear();
282             freeSpace.clear();
283             totalSize = 0;
284             randomAccessFile.setLength(0);
285 
286             indexFile.delete();
287             indexFile.createNewFile();
288         }
289         catch (Exception e) {
290             // Clean up
291             log.error(" Cache: Could not rebuild disk store", e);
292             dispose();
293         }
294     }
295 
296 
297     /***
298      * Shuts down the disk store in preparation for cache shutdown <p/>If a VM crash happens, the shutdown hook will not
299      * run. The data file and the index file will be out of synchronisation. At initialisation we always delete the
300      * index file after we have read the elements, so that it has a zero length. On a dirty restart, it still will have
301      * and the data file will automatically be deleted, thus preserving safety.
302      */
303     public synchronized void dispose() {
304 
305         //set allready in case some concurrent access
306         isDisposed = true;
307         // Close the cache
308         try {
309             //Flush the spool if persistent, so we don't lose any data.
310             writeIndex();
311             //Clear in-memory data structures
312 
313             diskElements.clear();
314             freeSpace.clear();
315             if (randomAccessFile != null) {
316                 randomAccessFile.close();
317             }
318 
319         }
320         catch (Exception e) {
321             log.error("Cache: Could not shut down disk cache", e);
322         }
323         finally {
324             randomAccessFile = null;
325         }
326     }
327 
328     /***
329      * Writes the Index to disk on shutdown <p/>The index consists of the elements Map and the freeSpace List <p/>Note
330      * that the cache is locked for the entire time that the index is being written
331      */
332     private synchronized void writeIndex() throws IOException {
333 
334         ObjectOutputStream objectOutputStream = null;
335         try {
336             FileOutputStream fout = new FileOutputStream(indexFile);
337             objectOutputStream = new ObjectOutputStream(fout);
338             objectOutputStream.writeObject(diskElements);
339             objectOutputStream.writeObject(freeSpace);
340         }
341         finally {
342             if (objectOutputStream != null) {
343                 objectOutputStream.flush();
344                 objectOutputStream.close();
345             }
346 
347         }
348     }
349 
350     /***
351      * Reads Index to disk on startup. <p/>if the index file does not exist, it creates a new one. <p/>Note that the
352      * cache is locked for the entire time that the index is being written
353      */
354     private synchronized void readIndex() throws IOException {
355         ObjectInputStream objectInputStream = null;
356         FileInputStream fin = null;
357         if (indexFile.exists() && persistant) {
358             try {
359                 fin = new FileInputStream(indexFile);
360                 objectInputStream = new ObjectInputStream(fin);
361                 diskElements = (HashedMap) objectInputStream.readObject();
362                 freeSpace = (ArrayList) objectInputStream.readObject();
363             }
364             catch (StreamCorruptedException e) {
365                 log.error("Corrupt index file. Creating new index.");
366 
367                 createNewIndexFile();
368             }
369             catch (IOException e) {
370                 log.error("IOException reading index. Creating new index. ");
371                 createNewIndexFile();
372             }
373             catch (ClassNotFoundException e) {
374                 log.error("Class loading problem reading index. Creating new index. ", e);
375                 createNewIndexFile();
376             }
377             finally {
378                 try {
379                     if (objectInputStream != null) {
380                         objectInputStream.close();
381                     } else if (fin != null) {
382                         fin.close();
383                     }
384                 }
385                 catch (IOException e) {
386                     log.error("Problem closing the index file.");
387                 }
388             }
389         } else {
390             createNewIndexFile();
391         }
392     }
393 
394     private void createNewIndexFile() throws IOException {
395         if (indexFile.exists()) {
396             indexFile.delete();
397             if (log.isDebugEnabled()) {
398                 log.debug("Index file " + indexFile + " deleted.");
399             }
400         }
401         if (indexFile.createNewFile()) {
402             if (log.isDebugEnabled()) {
403                 log.debug("Index file " + indexFile + " created successfully");
404             }
405         } else {
406             throw new IOException("Index file " + indexFile + " could not created.");
407         }
408     }
409 
410     /***
411      * Allocates a free block.
412      */
413     private DiskElement findFreeBlock(final int length) {
414         for (int i = 0; i < freeSpace.size(); i++) {
415             final DiskElement element = (DiskElement) freeSpace.get(i);
416             if (element.blockSize >= length) {
417                 freeSpace.remove(i);
418                 return element;
419             }
420         }
421         return null;
422     }
423 
424     /***
425      * Returns a {@link String}representation of the {@link DiskCaptchaBuffer}
426      */
427     public String toString() {
428         StringBuffer sb = new StringBuffer();
429         sb.append("[ dataFile = ").append(dataFile.getAbsolutePath()).append(", totalSize=")
430                 .append(totalSize).append(", status=").append(isInitalized).append(" ]");
431         return sb.toString();
432     }
433 
434     /***
435      * A reference to an on-disk elements.
436      */
437     private static class DiskElement implements Serializable {
438         /***
439          * the file pointer
440          */
441         private long position;
442 
443         /***
444          * The size used for data.
445          */
446         private int payloadSize;
447 
448         /***
449          * the size of this element.
450          */
451         private int blockSize;
452 
453     }
454 
455     /***
456      * @return the total size of the data file and the index file, in bytes.
457      */
458     public long getTotalFileSize() {
459         return getDataFileSize() + getIndexFileSize();
460     }
461 
462     /***
463      * @return the size of the data file in bytes.
464      */
465     public long getDataFileSize() {
466         return dataFile.length();
467     }
468 
469     /***
470      * The design of the layout on the data file means that there will be small gaps created when DiskElements are
471      * reused.
472      *
473      * @return the sparseness, measured as the percentage of space in the Data File not used for holding data
474      */
475     public float calculateDataFileSparseness() {
476         return 1 - ((float) getUsedDataSize() / (float) getDataFileSize());
477     }
478 
479     /***
480      * When elements are deleted, spaces are left in the file. These spaces are tracked and are reused when new elements
481      * need to be written. <p/>This method indicates the actual size used for data, excluding holes. It can be compared
482      * with {@link #getDataFileSize()}as a measure of fragmentation.
483      */
484     public long getUsedDataSize() {
485         return totalSize;
486     }
487 
488     /***
489      * @return the size of the index file, in bytes.
490      */
491     public long getIndexFileSize() {
492         if (indexFile == null) {
493             return 0;
494         } else {
495             return indexFile.length();
496         }
497     }
498 
499     /***
500      * @return the file name of the data file where the disk store stores data, without any path information.
501      */
502     public String getDataFileName() {
503         return name + ".data";
504     }
505 
506     /***
507      * @return the file name of the index file, which maintains a record of elements and their addresses on the data
508      *         file, without any path information.
509      */
510     public String getIndexFileName() {
511         return name + ".index";
512     }
513 
514     /***
515      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#removeCaptcha()
516      */
517     public Captcha removeCaptcha() throws NoSuchElementException {
518         if (isDisposed) return null;
519         return removeCaptcha(Locale.getDefault());
520     }
521 
522     /***
523      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#removeCaptcha(int)
524      */
525     public Collection removeCaptcha(int number) {
526         if (isDisposed) return null;
527         log.debug("Entering removeCaptcha(int number) ");
528         Collection c = null;
529         try {
530             c = remove(number, Locale.getDefault());
531         }
532         catch (IOException e) {
533 
534             throw new CaptchaException(e);
535         }
536         return c;
537     }
538 
539     /***
540      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#putCaptcha(com.octo.captcha.Captcha)
541      */
542     public void putCaptcha(Captcha captcha) {
543         log.debug("Entering putCaptcha(Captcha captcha)");
544         putCaptcha(captcha, Locale.getDefault());
545     }
546 
547     /***
548      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#putAllCaptcha(java.util.Collection)
549      */
550     public void putAllCaptcha(Collection captchas) {
551         log.debug("Entering putAllCaptcha()");
552 
553         putAllCaptcha(captchas, Locale.getDefault());
554     }
555 
556     /***
557      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#size()
558      */
559     public int size() {
560         if (!isInitalized) return 0;
561         int total = 0;
562         MapIterator it = diskElements.mapIterator();
563         while (it.hasNext()) {
564             it.next();
565             total += ((LinkedList) it.getValue()).size();
566         }
567         return total;
568     }
569 
570     /***
571      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#maxSize()
572      */
573     public int maxSize() {
574         return (int) this.maxDataSize;
575     }
576 
577     /***
578      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#removeCaptcha(java.util.Locale)
579      */
580     public Captcha removeCaptcha(Locale locale) throws NoSuchElementException {
581         log.debug("entering removeCaptcha(Locale locale)");
582 
583         Collection captchas = null;
584         try {
585             captchas = remove(1, locale);
586         }
587         catch (IOException e) {
588             throw new CaptchaException(e);
589         }
590         if (captchas.size() == 0) {
591             throw new NoSuchElementException();
592         }
593         return (Captcha) captchas.toArray()[0];
594     }
595 
596     /***
597      * @see CaptchaBuffer#removeCaptcha(int, java.util.Locale)
598      */
599     public Collection removeCaptcha(int number, Locale locale) {
600         if (isDisposed) return null;
601 
602         try {
603             return remove(number, locale);
604         }
605         catch (IOException e) {
606             throw new CaptchaException(e);
607         }
608     }
609 
610     /***
611      * @see CaptchaBuffer#putCaptcha(com.octo.captcha.Captcha,
612      *      java.util.Locale)
613      */
614     public void putCaptcha(Captcha captcha, Locale locale) {
615         if (isDisposed) return;
616 
617         try {
618             store(captcha, locale);
619         }
620         catch (IOException e) {
621             throw new CaptchaException(e);
622         }
623     }
624 
625     /***
626      * @see CaptchaBuffer#putAllCaptcha(java.util.Collection,
627      *      java.util.Locale)
628      */
629     public void putAllCaptcha(Collection captchas, Locale locale) {
630         if (isDisposed) return;
631         try {
632             store(captchas, locale);
633             log.debug("trying to store " + captchas.size());
634         }
635         catch (IOException e) {
636             throw new CaptchaException(e);
637         }
638     }
639 
640     /***
641      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#size(java.util.Locale)
642      */
643     public int size(Locale locale) {
644         if (!isInitalized || isDisposed) return 0;
645         return ((LinkedList) diskElements.get(locale)).size();
646     }
647 
648     /***
649      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#clear()
650      */
651     public void clear() {
652         try {
653             clearFile();
654         }
655         catch (IOException e) {
656             throw new CaptchaException(e);
657         }
658 
659     }
660 
661     /***
662      * @see com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer#getLocales()
663      */
664     public Collection getLocales() {
665         if (isDisposed) return null;
666         return diskElements.keySet();
667     }
668 
669 }