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   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;//OPTIMIZATION STUFF!
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         // this may cause a captcha disparition if a new captcha is asked between
246         // this call and the effective removing from the store!
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                 //remove from times
254                 times.remove(id);
255                 //remove from ids
256                 store.removeCaptcha(id);
257                 //update stats
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         //empty the store
275         this.store.empty();
276         //And the timestamps
277         this.times = new FastHashMap();
278     }
279 
280 
281     private Collection getGarbageCollectableCaptchaIds(long now) {
282 
283         //construct a new collection in order to avoid iterations synchronization pbs :
284         // this may cause a captcha disparition if a new captcha is asked between
285         // this call and the effective removing from the store!
286         HashSet garbageCollectableCaptchas = new HashSet();
287 
288         //the time limit under which captchas are collectable
289         long limit = now - 1000 * getMinGuarantedStorageDelayInSeconds();
290         if (limit > oldestCaptcha) {
291             // iterate to find out if the captcha is perimed
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         //remove from local after because validate may throw an exception if id is not found
369         this.times.remove(ID);
370         //update stats
371         if (valid.booleanValue()) {
372             numberOfCorrectResponse++;
373         } else {
374             numberOfUncorrectResponse++;
375         }
376         return valid;
377     }
378 
379 
380 }