1
2
3
4
5
6
7 package com.octo.captcha.service;
8
9 import com.octo.captcha.Captcha;
10 import com.octo.captcha.engine.CaptchaEngine;
11 import com.octo.captcha.service.captchastore.CaptchaStore;
12 import org.apache.commons.collections.FastHashMap;
13
14 import java.util.*;
15
16 /***
17 * This class provides default implementation for the management interface. It uses an HashMap to store the timestamps
18 * for garbage collection.
19 *
20 * @author <a href="mailto:mag@jcaptcha.net">Marc-Antoine Garrigue</a>
21 * @version 1.0
22 */
23 public abstract class AbstractManageableCaptchaService
24 extends AbstractCaptchaService
25 implements AbstractManageableCaptchaServiceMBean, CaptchaService {
26
27
28 private int minGuarantedStorageDelayInSeconds;
29 private int captchaStoreMaxSize;
30
31 private int captchaStoreSizeBeforeGarbageCollection;
32
33 private int numberOfGeneratedCaptchas = 0;
34 private int numberOfCorrectResponse = 0;
35 private int numberOfUncorrectResponse = 0;
36 private int numberOfGarbageCollectedCaptcha = 0;
37
38 private FastHashMap times;
39
40 private long oldestCaptcha = 0;
41
42
43 protected AbstractManageableCaptchaService(CaptchaStore captchaStore, com.octo.captcha.engine.CaptchaEngine captchaEngine,
44 int minGuarantedStorageDelayInSeconds, int maxCaptchaStoreSize) {
45 super(captchaStore, captchaEngine);
46
47 this.setCaptchaStoreMaxSize(maxCaptchaStoreSize);
48 this.setMinGuarantedStorageDelayInSeconds(minGuarantedStorageDelayInSeconds);
49 this.setCaptchaStoreSizeBeforeGarbageCollection((int) Math.round(0.8 * maxCaptchaStoreSize));
50 times = new FastHashMap();
51 }
52
53 protected AbstractManageableCaptchaService(CaptchaStore captchaStore, com.octo.captcha.engine.CaptchaEngine captchaEngine,
54 int minGuarantedStorageDelayInSeconds, int maxCaptchaStoreSize, int captchaStoreLoadBeforeGarbageCollection) {
55 this(captchaStore, captchaEngine, minGuarantedStorageDelayInSeconds, maxCaptchaStoreSize);
56 if (maxCaptchaStoreSize < captchaStoreLoadBeforeGarbageCollection)
57 throw new IllegalArgumentException("the max store size can't be less than garbage collection size. if you want to disable garbage" +
58 " collection (this is not recommended) you may set them equals (max=garbage)");
59 this.setCaptchaStoreSizeBeforeGarbageCollection(captchaStoreLoadBeforeGarbageCollection);
60
61 }
62
63 /***
64 * Get the fully qualified class name of the concrete CaptchaEngine used by the service.
65 *
66 * @return the fully qualified class name of the concrete CaptchaEngine used by the service.
67 */
68 public String getCaptchaEngineClass() {
69 return this.engine.getClass().getName();
70 }
71
72 /***
73 * Set the fully qualified class name of the concrete CaptchaEngine used by the service
74 *
75 * @param theClassName the fully qualified class name of the CaptchaEngine used by the service
76 *
77 * @throws IllegalArgumentException if className can't be used as the service CaptchaEngine, either because it can't
78 * be instanciated by the service or it is not a ImageCaptchaEngine concrete
79 * class.
80 */
81 public void setCaptchaEngineClass(String theClassName)
82 throws IllegalArgumentException {
83 try {
84 Object engine = Class.forName(theClassName).newInstance();
85 if (engine instanceof com.octo.captcha.engine.CaptchaEngine) {
86 this.engine = (com.octo.captcha.engine.CaptchaEngine) engine;
87 } else {
88 throw new IllegalArgumentException("Class is not instance of CaptchaEngine! "
89 + theClassName);
90 }
91 } catch (InstantiationException e) {
92 throw new IllegalArgumentException(e.getMessage());
93 } catch (IllegalAccessException e) {
94 throw new IllegalArgumentException(e.getMessage());
95 } catch (ClassNotFoundException e) {
96 throw new IllegalArgumentException(e.getMessage());
97 } catch (RuntimeException e) {
98 throw new IllegalArgumentException(e.getMessage());
99 }
100 }
101
102 /***
103 * @return the engine served by this service
104 */
105 public CaptchaEngine getEngine() {
106 return this.engine;
107 }
108
109 /***
110 * Updates the engine served by this service
111 */
112 public void setCaptchaEngine(CaptchaEngine engine) {
113 this.engine = engine;
114 }
115
116 /***
117 * Get the minimum delay (in seconds) a client can be assured that a captcha generated by the service can be
118 * retrieved and a response to its challenge tested
119 *
120 * @return the maximum delay in seconds
121 */
122 public int getMinGuarantedStorageDelayInSeconds() {
123 return minGuarantedStorageDelayInSeconds;
124 }
125
126 /***
127 * set the minimum delay (in seconds)a client can be assured that a captcha generated by the service can be
128 * retrieved and a response to its challenge tested
129 *
130 * @param theMinGuarantedStorageDelayInSeconds
131 * the minimum guaranted delay
132 */
133 public void setMinGuarantedStorageDelayInSeconds(int theMinGuarantedStorageDelayInSeconds) {
134 this.minGuarantedStorageDelayInSeconds = theMinGuarantedStorageDelayInSeconds;
135 }
136
137
138 /***
139 * Get the number of captcha generated since the service is up WARNING : this value won't be significant if the real
140 * number is > Long.MAX_VALUE
141 *
142 * @return the number of captcha generated since the service is up
143 */
144 public long getNumberOfGeneratedCaptchas() {
145 return numberOfGeneratedCaptchas;
146 }
147
148 /***
149 * Get the number of correct responses to captcha challenges since the service is up. WARNING : this value won't be
150 * significant if the real number is > Long.MAX_VALUE
151 *
152 * @return the number of correct responses since the service is up
153 */
154 public long getNumberOfCorrectResponses() {
155 return numberOfCorrectResponse;
156 }
157
158 /***
159 * Get the number of uncorrect responses to captcha challenges since the service is up. WARNING : this value won't
160 * be significant if the real number is > Long.MAX_VALUE
161 *
162 * @return the number of uncorrect responses since the service is up
163 */
164 public long getNumberOfUncorrectResponses() {
165 return numberOfUncorrectResponse;
166 }
167
168 /***
169 * Get the curent size of the captcha store
170 *
171 * @return the size of the captcha store
172 */
173 public int getCaptchaStoreSize() {
174 return this.store.getSize();
175 }
176
177 /***
178 * Get the number of captchas that can be garbage collected in the captcha store
179 *
180 * @return the number of captchas that can be garbage collected in the captcha store
181 */
182 public int getNumberOfGarbageCollectableCaptchas() {
183 return getGarbageCollectableCaptchaIds(System.currentTimeMillis()).size();
184 }
185
186
187 /***
188 * Get the number of captcha garbage collected since the service is up WARNING : this value won't be significant if
189 * the real number is > Long.MAX_VALUE
190 *
191 * @return the number of captcha garbage collected since the service is up
192 */
193 public long getNumberOfGarbageCollectedCaptcha() {
194 return numberOfGarbageCollectedCaptcha;
195 }
196
197 /***
198 * @return the max captchaStore load before garbage collection of the store
199 */
200 public int getCaptchaStoreSizeBeforeGarbageCollection() {
201 return captchaStoreSizeBeforeGarbageCollection;
202 }
203
204 /***
205 * max captchaStore size before garbage collection of the store
206 */
207 public void setCaptchaStoreSizeBeforeGarbageCollection(int captchaStoreSizeBeforeGarbageCollection) {
208 if (this.captchaStoreMaxSize <
209 captchaStoreSizeBeforeGarbageCollection)
210 throw new IllegalArgumentException("the max store size can't be less than garbage collection "
211 + "size. if you want to disable garbage" +
212 " collection (this is not recommended) you may "
213 + "set them equals (max=garbage)");
214
215 this.captchaStoreSizeBeforeGarbageCollection =
216 captchaStoreSizeBeforeGarbageCollection;
217 }
218
219 /***
220 * This max size is used by the service : it will throw a CaptchaServiceException if the store is full when a client
221 * ask for a captcha.
222 */
223 public void setCaptchaStoreMaxSize(int size) {
224 if (size < this.captchaStoreSizeBeforeGarbageCollection)
225 throw new IllegalArgumentException("the max store size can't "
226 + "be less than garbage collection size. if you want "
227 + "to disable garbage" +
228 " collection (this is not recommended) you may "
229 + "set them equals (max=garbage)");
230 this.captchaStoreMaxSize = size;
231 }
232
233 /***
234 * @return the desired max size of the captcha store
235 */
236 public int getCaptchaStoreMaxSize() {
237 return this.captchaStoreMaxSize;
238 }
239
240 /***
241 * Garbage collect the captcha store, means all old captcha (captcha in the store wich has been stored more than the
242 * MinGuarantedStorageDelayInSecond
243 */
244 protected void garbageCollectCaptchaStore(Iterator garbageCollectableCaptchaIds) {
245
246
247 long now = System.currentTimeMillis();
248 long limit = now - 1000 * minGuarantedStorageDelayInSeconds;
249
250 while (garbageCollectableCaptchaIds.hasNext()) {
251 String id = garbageCollectableCaptchaIds.next().toString();
252 if (((Long) times.get(id)).longValue() < limit) {
253
254 times.remove(id);
255
256 store.removeCaptcha(id);
257
258 this.numberOfGarbageCollectedCaptcha++;
259 }
260 }
261 }
262
263 public void garbageCollectCaptchaStore() {
264 long now = System.currentTimeMillis();
265 Collection garbageCollectableCaptchaIds = getGarbageCollectableCaptchaIds(now);
266 this.garbageCollectCaptchaStore(garbageCollectableCaptchaIds.iterator());
267 }
268
269
270 /***
271 * Empty the Store
272 */
273 public void emptyCaptchaStore() {
274
275 this.store.empty();
276
277 this.times = new FastHashMap();
278 }
279
280
281 private Collection getGarbageCollectableCaptchaIds(long now) {
282
283
284
285
286 HashSet garbageCollectableCaptchas = new HashSet();
287
288
289 long limit = now - 1000 * getMinGuarantedStorageDelayInSeconds();
290 if (limit > oldestCaptcha) {
291
292 Iterator ids = new HashSet(times.keySet()).iterator();
293 while (ids.hasNext()) {
294 String id = (String) ids.next();
295 long captchaDate = ((Long) times.get(id)).longValue();
296 oldestCaptcha = Math.min(captchaDate, oldestCaptcha == 0 ? captchaDate : oldestCaptcha);
297 if (captchaDate < limit) {
298 garbageCollectableCaptchas.add(id);
299 }
300 }
301 }
302 return garbageCollectableCaptchas;
303 }
304
305 //********
306 ///Overriding business methods to add some stats and store management hooks
307 ///****
308
309 protected Captcha generateAndStoreCaptcha(Locale locale, String ID) {
310
311 //if the store is full try to garbage collect
312 if (isCaptchaStoreFull()) {
313 //see if possible
314 long now = System.currentTimeMillis();
315 Collection garbageCollectableCaptchaIds = getGarbageCollectableCaptchaIds(now);
316 if (garbageCollectableCaptchaIds.size() > 0) {
317 //possible collect an rerun
318 garbageCollectCaptchaStore(garbageCollectableCaptchaIds.iterator());
319 return this.generateAndStoreCaptcha(locale, ID);
320 } else {
321 //impossible ! has to wait
322 throw new CaptchaServiceException("Store is full, try to increase CaptchaStore Size or" +
323 "to decrease time out, or to decrease CaptchaStoreSizeBeforeGrbageCollection");
324 }
325 }
326
327 if (isCaptchaStoreQuotaReached()) {
328 //then garbage collect
329 garbageCollectCaptchaStore();
330 }
331 return generateCountTimeStampAndStoreCaptcha(ID, locale);
332 }
333
334 private Captcha generateCountTimeStampAndStoreCaptcha(String ID, Locale locale) {
335 //update stats
336 numberOfGeneratedCaptchas++;
337 //mark as now
338 Long now = new Long(System.currentTimeMillis());
339 //store in my timestampeds ids
340 this.times.put(ID, now);
341 //retrieve and store cpatcha
342 Captcha captcha = super.generateAndStoreCaptcha(locale, ID);
343 return captcha;
344 }
345
346
347 protected boolean isCaptchaStoreFull() {
348 return getCaptchaStoreMaxSize() == 0 ? false : getCaptchaStoreSize() >= getCaptchaStoreMaxSize();
349 }
350
351 protected boolean isCaptchaStoreQuotaReached() {
352 return getCaptchaStoreSize() >= getCaptchaStoreSizeBeforeGarbageCollection();
353 }
354
355 /**
356 * Method to validate a response to the challenge corresponding to the given ticket and remove the coresponding
357 * captcha from the store.
358 *
359 * @param ID the ticket provided by the buildCaptchaAndGetID method
360 *
361 * @return true if the response is correct, false otherwise.
362 *
363 * @throws CaptchaServiceException if the ticket is invalid
364 */
365 public Boolean validateResponseForID(String ID, Object response) throws CaptchaServiceException {
366
367 Boolean valid = super.validateResponseForID(ID, response);
368
369 this.times.remove(ID);
370
371 if (valid.booleanValue()) {
372 numberOfCorrectResponse++;
373 } else {
374 numberOfUncorrectResponse++;
375 }
376 return valid;
377 }
378
379
380 }