1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
148 if (!diskElements.containsKey(locale)) {
149 return collection;
150 }
151
152 try {
153 while (!diskEmpty && index < number) {
154
155
156 try {
157 diskElement = (DiskElement) ((LinkedList) diskElements.get(locale))
158 .removeFirst();
159
160
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
196 for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
197 final Object element = iterator.next();
198
199
200 final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
201 final ObjectOutputStream objstr = new ObjectOutputStream(outstr);
202 objstr.writeObject(element);
203 objstr.close();
204
205
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
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
224
225
226
227
228
229
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
238
239 randomAccessFile.seek(diskElement.position);
240
241
242
243
244 randomAccessFile.write(buffer);
245
246
247 diskElement.payloadSize = buffer.length;
248 totalSize += buffer.length;
249
250
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
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
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
306 isDisposed = true;
307
308 try {
309
310 writeIndex();
311
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 }