/*
* 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;
import com.octo.captcha.Captcha;
import com.octo.captcha.CaptchaException;
import com.octo.captcha.CaptchaFactory;
import com.octo.captcha.engine.CaptchaEngine;
import com.octo.captcha.engine.CaptchaEngineException;
import com.octo.captcha.engine.bufferedengine.buffer.CaptchaBuffer;
import org.apache.commons.collections.MapIterator;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.*;
/**
* Abstact class that encapsulate a CaptchaEngine to allow buffering. A BufferedEngineContainer has mainly one function
* : to provide cached captchas to increase performances. This is done through two embedded buffers : a disk buffer and
* a memory buffer. When captchas are requested, the bufferedEngine take them either from the memory buffer if not empty
* or directly from the engine. Some good periods are defined with a scheduler to feed the disk buffer with captchas and
* some others to swap captchas from the disk buffer to the memory buffer.
*
* @author Benoit Doumas
*/
public abstract class BufferedEngineContainer implements CaptchaEngine {
private static final Log log = LogFactory.getLog(BufferedEngineContainer.class.getName());
protected CaptchaBuffer persistentBuffer = null;
protected CaptchaBuffer volatileBuffer = null;
protected CaptchaEngine engine = null;
protected ContainerConfiguration config = null;
protected int volatileMemoryHits = 0;
protected int persistentMemoryHits = 0;
protected int persistentToVolatileSwaps = 0;
protected int persistentFeedings = 0;
private boolean shutdownCalled = false;
/**
* Construct an BufferedEngineContainer with and Captcha engine, a memory buffer, a diskBuffer and a
* ContainerConfiguration.
*
* @param engine engine to generate captcha for buffers
* @param volatileBuffer the memory buffer, which store captcha and provide a fast access to them
* @param persistentBuffer the disk buffer which store captchas not in a volatil and memory consuming way
* @param containerConfiguration the container configuration
*/
public BufferedEngineContainer(CaptchaEngine engine, CaptchaBuffer volatileBuffer,
CaptchaBuffer persistentBuffer, ContainerConfiguration containerConfiguration) {
this.engine = engine;
if (engine == null) {
throw new CaptchaEngineException("impossible to build a BufferedEngineContainer with a null engine");
}
this.volatileBuffer = volatileBuffer;
if (persistentBuffer == null) {
throw new CaptchaEngineException("impossible to build a BufferedEngineContainer with a null volatileBuffer");
}
this.persistentBuffer = persistentBuffer;
if (persistentBuffer == null) {
throw new CaptchaEngineException("impossible to build a BufferedEngineContainer with a null persistentBuffer");
}
this.config = containerConfiguration;
if (config == null) {
throw new CaptchaEngineException("impossible to build a BufferedEngineContainer with a null configuration");
}
//define hook when JVM is shutdown
Shutdown sh = new Shutdown();
Runtime.getRuntime().addShutdownHook(sh);
}
/**
* @see com.octo.captcha.engine.CaptchaEngine#getNextCaptcha()
*/
public Captcha getNextCaptcha() {
log.debug("entering getNextCaptcha()");
return getNextCaptcha(config.getDefaultLocale());
}
/**
* @see com.octo.captcha.engine.CaptchaEngine#getNextCaptcha(java.util.Locale)
*/
public Captcha getNextCaptcha(Locale locale) {
log.debug("entering getNextCaptcha(Locale locale)");
Captcha captcha = null;
locale = resolveLocale(locale);
try {
captcha = volatileBuffer.removeCaptcha(locale);
} catch (NoSuchElementException e) {
log.debug("no captcha under this locale", e);
}
if (captcha == null) {
//get from engine directly
captcha = engine.getNextCaptcha(locale);
log.debug("get captcha from engine");
if (config.isServeOnlyConfiguredLocales()) {
log.warn("captcha is directly built from engine, try to increase the swap frequency or the persistant buffer size");
}
} else {
log.debug("get captcha from memory");
//stats
volatileMemoryHits++;
}
return captcha;
}
/**
* @return captcha factories used by this engine
*/
public CaptchaFactory[] getFactories() {
return this.engine.getFactories();
}
/**
* @param factories new captcha factories for this engine
*/
public void setFactories(CaptchaFactory[] factories) {
this.engine.setFactories(factories);
}
/**
* Helper for locale
*/
private Locale resolveLocale(Locale locale) {
if (!config.isServeOnlyConfiguredLocales()) {
return locale;
} else {
if (this.config.getLocaleRatio().containsKey(locale)) {
return locale;
//try to resolve by language
} else if (this.config.getLocaleRatio().containsKey(locale.getLanguage())) {
return new Locale(locale.getLanguage());
} else {
return config.getDefaultLocale();
}
}
}
/**
* Method launch by a scheduler to swap captcha from disk buffer to the memory buffer. The ratio of swaping for each
* locale is defined in the configuration component.
*/
public void swapCaptchasFromPersistentToVolatileMemory() {
log.debug("entering swapCaptchasFromDiskBufferToMemoryBuffer()");
MapIterator it = config.getLocaleRatio().mapIterator();
//construct the map of swap size by locales;
HashedMap captchasRatios = new HashedMap();
while (it.hasNext()) {
Locale locale = (Locale) it.next();
double ratio = ((Double) it.getValue()).doubleValue();
int ratioCount = (int) Math.round(config.getSwapSize().intValue() * ratio);
//get the reminding size corresponding to the ratio
int diff = (int) Math.round((config.getMaxVolatileMemorySize().intValue() - this.volatileBuffer
.size()) * ratio);
diff = diff < 0 ? 0 : diff;
int toSwap = (diff < ratioCount) ? diff : ratioCount;
captchasRatios.put(locale, new Integer(toSwap));
}
//get them from persistent buffer
Iterator captchasRatiosit = captchasRatios.mapIterator();
while (captchasRatiosit.hasNext() && !shutdownCalled) {
Locale locale = (Locale) captchasRatiosit.next();
int swap = ((Integer) captchasRatios.get(locale)).intValue();
if (log.isDebugEnabled()) {
log.debug("try to swap " + swap + " Captchas from persistent to volatile memory with locale : "
+ locale.toString());
}
Collection temp = this.persistentBuffer.removeCaptcha(swap, locale);
this.volatileBuffer.putAllCaptcha(temp, locale);
if (log.isDebugEnabled()) {
log.debug("swaped " + temp.size() + " Captchas from persistent to volatile memory with locale : "
+ locale.toString());
}
//stats
persistentMemoryHits += temp.size();
}
if (log.isDebugEnabled()) {
log.debug("new volatil Buffer size : " + volatileBuffer.size());
}
// stats
persistentToVolatileSwaps++;
}
/**
* Method launch by a scheduler to feed the disk buffer with captcha. The ratio of feeding for each locale is
* defined in the configuration component.
*/
public void feedPersistentBuffer() {
log.debug("entering feedPersistentBuffer()");
//evaluate the total feed size
int freePersistentBufferSize = config.getMaxPersistentMemorySize().intValue() - persistentBuffer.size();
freePersistentBufferSize = freePersistentBufferSize > 0 ? freePersistentBufferSize : 0;
int totalFeedsize = freePersistentBufferSize > config.getFeedSize().intValue() ? config.getFeedSize().intValue() : freePersistentBufferSize;
log.info("Starting feed. Total feed size = " + totalFeedsize);
//feed the buffer for each locale
MapIterator it = config.getLocaleRatio().mapIterator();
while (it.hasNext() && !shutdownCalled) {
Locale locale = (Locale) it.next();
double ratio = ((Double) it.getValue()).doubleValue();
int ratioCount = (int) Math.round(totalFeedsize * ratio);
if (log.isDebugEnabled()) {
log.debug("construct " + ratioCount + " captchas for locale " + locale.toString());
}
//batch build and store captchas
int toBuild = ratioCount;
while (toBuild > 0 && !shutdownCalled) {
int batch = toBuild > config.getFeedBatchSize().intValue() ? config.getFeedBatchSize().intValue() : toBuild;
ArrayList captchas = new ArrayList(batch);
//build captchas, batch sized
int builded = 0;
for (int i = 0; i < batch; i++) {
try {
captchas.add(engine.getNextCaptcha(locale));
builded++;
} catch (CaptchaException e) {
log.warn("Error during captcha construction, skip this one : ", e);
}
}
//persist
persistentBuffer.putAllCaptcha(captchas, locale);
if (log.isInfoEnabled()) {
log.info("feeded persistent buffer with " + builded + " captchas for locale " + locale);
}
toBuild -= builded;
}
}
log.info("Stopping feed : feeded persitentBuffer with : " + totalFeedsize + " captchas");
log.info("Stopping feed : resulting persitentBuffer size : " + persistentBuffer.size());
persistentFeedings++;
}
public ContainerConfiguration getConfig() {
return config;
}
public CaptchaBuffer getPersistentBuffer() {
return persistentBuffer;
}
public Integer getPersistentFeedings() {
return new Integer(persistentFeedings);
}
public Integer getPersistentMemoryHits() {
return new Integer(persistentMemoryHits);
}
public Integer getPersistentToVolatileSwaps() {
return new Integer(persistentToVolatileSwaps);
}
public CaptchaBuffer getVolatileBuffer() {
return volatileBuffer;
}
public Integer getVolatileMemoryHits() {
return new Integer(volatileMemoryHits);
}
class Shutdown extends Thread {
public Shutdown() {
super();
}
public void run() {
log.info("Buffered engine shutdown thread started");
shutdownCalled = true;
try {
closeBuffers();
} catch (Exception ee) {
ee.printStackTrace();
}
}
}
public void closeBuffers() {
this.persistentBuffer.dispose();
this.volatileBuffer.dispose();
log.info("Buffers disposed");
}
}