1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.caleigo.core.service;
20
21
22 import java.util.*;
23
24 import org.caleigo.core.*;
25 import org.caleigo.core.exception.*;
26 import org.caleigo.toolkit.log.*;
27
28 /*** The CachedDataService is a wrapper around any other IDataService
29 * implementation to wich it adds transparent cache functionality.
30 *
31 * @author Dennis Zikovic
32 * @version 1.00
33 *
34 *//*
35 *
36 * WHEN WHO WHY & WHAT
37 * -----------------------------------------------------------------------------
38 * 2002-07-19 Dennis Zikovic Creation
39 */
40 public class CachedDataService extends AbstractDataService
41 {
42
43 protected static final int QUERY_LIST_LIMIT = 100;
44 protected static final int DEFAULT_MINIMUM_TABLE_CACHE_TIME = 15*60;
45 protected static final int DEFAULT_MINIMUM_ENTITY_CACHE_TIME = 1*60;
46
47
48 private static int sMinimumTableCacheTime = DEFAULT_MINIMUM_TABLE_CACHE_TIME;
49 private static int sMinimumEntityCacheTime = DEFAULT_MINIMUM_ENTITY_CACHE_TIME;
50
51
52 protected final IDataService mSourceDataService;
53
54 private Map mTableCache;
55 private LinkedList mEntityQueryList;
56
57
58
59 public static void setMinimumTableCacheTime(int minimumTableCacheTime)
60 {
61 sMinimumTableCacheTime = minimumTableCacheTime;
62 }
63
64 public static int getMinimumTableCacheTime()
65 {
66 return sMinimumTableCacheTime;
67 }
68
69 public static void setMinimumEntityCacheTime(int minimumEntityCacheTime)
70 {
71 sMinimumEntityCacheTime = minimumEntityCacheTime;
72 }
73
74 public static int getMinimumEntityCacheTime()
75 {
76 return sMinimumEntityCacheTime;
77 }
78
79 /*** Access methods that returns true if the provided entity descriptor
80 * can be cached in the table cache.
81 */
82 public static boolean isTableCachable(IEntityDescriptor entityDescriptor)
83 {
84 return entityDescriptor.isCacheable() && entityDescriptor.getCacheTime()>=sMinimumTableCacheTime;
85 }
86
87
88
89 /*** Creates a new instance of CachedDataService
90 */
91 public CachedDataService(IDataService sourceDataService)
92 {
93 super(sourceDataService.getServiceType(), sourceDataService.getServiceIdentity(), sourceDataService.getDataSourceDescriptor());
94 mSourceDataService = sourceDataService;
95
96 mTableCache = new HashMap(100);
97 mEntityQueryList = new LinkedList();
98
99 }
100
101
102 public IDataTransaction newTransaction()
103 {
104 return new CachedDataTransaction();
105 }
106
107 /*** Should return true if the service is online and reponding to calls.
108 */
109 public boolean ping()
110 {
111 return mSourceDataService.ping();
112 }
113
114
115
116 /*** Completely resets the cache and "unloads" all cached data.
117 */
118 public void resetCache()
119 {
120 mTableCache.clear();
121 mEntityQueryList.clear();
122 }
123
124 /*** Unloads the entity table indentified by the provided entity descriptor.
125 */
126 public void unloadEntityTable(IEntityDescriptor entityDescriptor)
127 {
128 mTableCache.remove(entityDescriptor);
129 }
130
131 /*** This method loads a complete entity table from the wrapped IDataService
132 * and stores it as a selection in the cache´s hashtable. This method can
133 *be used to "prepare" the cache.
134 */
135 public void cacheEntityTable(IEntityDescriptor entityDescriptor)
136 {
137
138 if(this.isTableCached(entityDescriptor))
139 return;
140
141 try
142 {
143 long startTime = System.currentTimeMillis();
144 ISelection selection = mSourceDataService.loadSelection(entityDescriptor, null);
145 mTableCache.put(entityDescriptor, new CacheEntry(selection));
146 Log.print(this, "Caching entity table "+entityDescriptor+", "+selection.size()+" entities loaded in "+(System.currentTimeMillis()-startTime)+" ms.");
147 }
148 catch(DataServiceException e)
149 {
150 Log.printError(this, "Failed to cache entity table for "+entityDescriptor, e.getCause());
151 }
152 }
153
154
155
156 /*** Access method that returns the IDataService object that cache
157 * functionality is added to by this class.
158 */
159 public IDataService getSourceDataService()
160 {
161 return mSourceDataService;
162 }
163
164 /*** Access methods that returns true if the provided entity descriptor
165 * is currently cached in the table cache.
166 */
167 public boolean isTableCached(IEntityDescriptor entityDescriptor)
168 {
169
170 CacheEntry entry = (CacheEntry)mTableCache.get(entityDescriptor);
171
172
173 if(entry!=null && !entry.isValid())
174 {
175
176 mTableCache.remove(entityDescriptor);
177 Log.print(this, "Removing outdated selection from table cache: "+entityDescriptor);
178 entry = null;
179 }
180
181 return entry!=null;
182 }
183
184 /*** Access method that retrieves a selection containing the entire stored
185 * table of entities for the provided entity descriptor. If the table is
186 * not present in the cache it will be loaded and stored there first.
187 * If the entity type is not cacheable then null is returned.
188 */
189 protected ISelection getCachedEntityTable(IEntityDescriptor entityDescriptor)
190 {
191
192 CacheEntry entry = (CacheEntry)mTableCache.get(entityDescriptor);
193
194
195 if(entry!=null && !entry.isValid())
196 {
197
198 mTableCache.remove(entityDescriptor);
199 Log.print(this, "Removing outdated selection from table cache: "+entityDescriptor);
200 entry = null;
201 }
202
203
204 if(entry==null && isTableCachable(entityDescriptor))
205 this.cacheEntityTable(entityDescriptor);
206
207
208 entry = (CacheEntry)mTableCache.get(entityDescriptor);
209 if(entry!=null)
210 return entry.getSelection();
211 else
212 return null;
213 }
214
215
216
217 /*** Help methods that returns true if the provided qualifier can be
218 * processed internally by the table cache.
219 */
220 public boolean isQualifierProcessable(Qualifier qualifier, IEntityDescriptor entityDescriptor)
221 {
222 return qualifier==null || qualifier.canDirectlyQualify(entityDescriptor);
223 }
224
225 /*** Loads a qualified entity from the cache.
226 */
227 protected void loadEntityFromCache(Qualifier qualifier, IEntity targetEntity)
228 {
229 ISelection selection = this.getCachedEntityTable(targetEntity.getEntityDescriptor());
230 if(selection!=null)
231 selection = selection.createSubSelection(qualifier);
232 if(selection!=null && selection.size()==1)
233 {
234
235 if(!DataAccessManager.getManager().hasReadAccess(targetEntity))
236 targetEntity.clear();
237 else
238 {
239 targetEntity.copyData(selection.getEntity(0));
240 targetEntity.setStatusFlag(IEntity.PERSISTENT);
241 targetEntity.clearStatusFlag(IEntity.DIRTY | IEntity.EMPTY);
242 }
243
244
245 }
246
247
248 }
249
250 /*** Loads a qualified selection from the cache.
251 */
252 protected void loadSelectionFromCache(IEntityDescriptor entityDescriptor, Qualifier qualifier, ISelection targetSelection)
253 {
254 ISelection selection = this.getCachedEntityTable(entityDescriptor);
255 if(selection!=null)
256 {
257 for(int j=0; j<selection.size(); j++)
258 if(qualifier==null || qualifier.doesQualify(selection.getEntity(j)))
259 {
260
261
262 if(DataAccessManager.getManager().hasReadAccess(selection.getEntity(j)))
263 targetSelection.addEntity(selection.getEntity(j));
264 }
265
266 }
267 }
268
269 /*** Loads a qualified selection from the cache using a data query.
270 */
271 protected void loadSelectionFromCache(DataQuery dataQuery, ISelection targetSelection)
272 {
273 this.loadSelectionFromCache(dataQuery.getEntityDescriptor(), dataQuery.getQualifier(), targetSelection);
274 if(targetSelection!=null && dataQuery.getEntityCollator()!=null)
275 targetSelection.sort(dataQuery.getEntityCollator());
276 }
277
278 /*** Updates the provided entity in the cache.
279 */
280 protected void refreshCachedEntity(IEntity entity)
281 {
282
283 if(entity.isEmpty() || !entity.isPersistent())
284 return;
285
286
287 ISelection selection = this.getCachedEntityTable(entity.getEntityDescriptor());
288 if(selection==null)
289 Log.printWarning(this, "Data cache refresh inconsistency for entity: "+entity);
290
291
292 int index = selection!=null ? selection.indexOf(entity) : -1;
293 if(index>=0)
294 {
295
296 selection.getEntity(index).copyData(entity);
297 entity.setStatusFlag(IEntity.PERSISTENT);
298 entity.clearStatusFlag(IEntity.DIRTY | IEntity.EMPTY);
299 Log.print(this, "Updating cached entity: "+entity);
300 }
301 else
302 {
303 selection.addEntity(entity);
304 Log.print(this, "Adding cached entity: "+entity);
305 }
306 }
307
308 /*** Deletes the provided entity from the cache.
309 */
310 protected void deleteCachedEntity(IEntity entity)
311 {
312 if(!entity.isPersistent())
313 {
314 ISelection selection = this.getCachedEntityTable(entity.getEntityDescriptor());
315 if(selection!=null)
316 {
317 this.getCachedEntityTable(entity.getEntityDescriptor()).removeEntity(entity);
318 Log.print(this, "Deleting cached entity: "+entity);
319 }
320 else
321 Log.printWarning(this, "Data cache refresh inconsistency for entity: "+entity);
322 }
323 }
324
325 protected boolean loadEntityFromQueryList(Qualifier qualifier, IEntity targetEntity)
326 {
327 if(targetEntity.getEntityDescriptor().getCacheTime()<=0 && qualifier.canDirectlyQualify(targetEntity.getEntityDescriptor()))
328 return false;
329
330 boolean found = false;
331 synchronized(mEntityQueryList)
332 {
333 CacheEntry entry;
334 Iterator it = mEntityQueryList.iterator();
335 ArrayList entitiesToMoveToFront = new ArrayList();
336 while(it.hasNext() && !found)
337 {
338 entry = (CacheEntry)it.next();
339 if(!entry.isValid())
340 it.remove();
341 else if(entry.getEntity().getEntityDescriptor()==targetEntity.getEntityDescriptor() && qualifier.doesQualify(entry.getEntity()))
342 {
343
344 if(!DataAccessManager.getManager().hasReadAccess(targetEntity))
345 targetEntity.clear();
346 else
347 {
348 targetEntity.copyData(entry.getEntity());
349 targetEntity.setStatusFlag(IEntity.PERSISTENT);
350 targetEntity.clearStatusFlag(IEntity.DIRTY | IEntity.EMPTY);
351 }
352
353 found = true;
354
355
356
357
358 entitiesToMoveToFront.add(entry);
359 }
360 }
361
362 for (int i = 0; i < entitiesToMoveToFront.size(); i++)
363 {
364 mEntityQueryList.remove(entitiesToMoveToFront.get(i));
365 mEntityQueryList.addFirst(entitiesToMoveToFront.get(i));
366 }
367 }
368 return found;
369 }
370
371 protected void addToQueryList(IEntity entity)
372 {
373 if(entity.isDirty() || !entity.isPersistent() || entity.getEntityDescriptor().getCacheTime()<=0)
374 return;
375
376 synchronized(mEntityQueryList)
377 {
378 mEntityQueryList.addFirst(new CacheEntry(entity));
379 Log.print(this, "Cached entity as recent: "+entity);
380 if(mEntityQueryList.size()>QUERY_LIST_LIMIT)
381 mEntityQueryList.removeLast();
382 }
383 }
384
385
386 protected class CachedDataTransaction extends AbstractDataTransaction
387 {
388
389 private IDataTransaction mSourceTransaction;
390
391
392 public CachedDataTransaction()
393 {
394 super(getTimeout());
395 }
396
397
398 public void commit() throws DataServiceException
399 {
400
401 Enumeration dataOperations = this.getOperations();
402
403
404 while(dataOperations.hasMoreElements())
405 {
406 DataOperation operation = (DataOperation)dataOperations.nextElement();
407 switch(operation.getOperationType())
408 {
409 case DataOperation.LOAD:
410 if(isTableCachable(operation.getEntity().getEntityDescriptor())
411 && isQualifierProcessable(operation.getQualifier(), operation.getEntity().getEntityDescriptor()))
412 loadEntityFromCache(operation.getQualifier(), operation.getEntity());
413 else if(!loadEntityFromQueryList(operation.getQualifier(), operation.getEntity()))
414 this.getSourceTransaction().addLoad(operation.getQualifier(), operation.getEntity());
415 break;
416 case DataOperation.QUERY:
417 if(isTableCachable(operation.getDataQuery().getEntityDescriptor())
418 && isQualifierProcessable(operation.getDataQuery().getQualifier(), operation.getDataQuery().getEntityDescriptor()))
419 loadSelectionFromCache(operation.getDataQuery(), operation.getEntitySelection());
420 else
421 this.getSourceTransaction().addLoadSelection(operation.getDataQuery(), operation.getEntitySelection());
422 break;
423 case DataOperation.STORE:
424 this.getSourceTransaction().addStore(operation.getEntity());
425 break;
426 case DataOperation.DELETE:
427 this.getSourceTransaction().addDelete(operation.getEntity());
428 break;
429 case DataOperation.REFRESH:
430 this.getSourceTransaction().addRefresh(operation.getEntity());
431 break;
432 }
433 }
434
435
436 if(mSourceTransaction!=null)
437 {
438 try
439 {
440
441 if (this.getJob() != null)
442 {
443
444 Object lock = new Object();
445 Job sourceTransactionJob = mSourceTransaction.commitAsynchroniesly(lock);
446 sourceTransactionJob.addProgressListener(
447 new IDataTransaction.ProgressAdapter()
448 {
449 public void jobFailed(ProgressEvent e)
450 {
451 getJob().fireJobFailedEvent(e.getJob().getException());
452 }
453
454 public void progressChanged(ProgressEvent e)
455 {
456 getJob().fireProgressChangedEvent(e.getMaxProgress(), e.getCurrentProgress());
457 }
458 });
459
460 while (!sourceTransactionJob.isFinished())
461 synchronized (lock)
462 {
463 lock.wait();
464 }
465 }
466 else
467 mSourceTransaction.commit();
468
469
470 dataOperations = this.getOperations();
471 while(dataOperations.hasMoreElements())
472 {
473 DataOperation operation = (DataOperation)dataOperations.nextElement();
474 switch(operation.getOperationType())
475 {
476 case DataOperation.LOAD:
477 if(!isTableCachable(operation.getEntity().getEntityDescriptor()))
478 addToQueryList(operation.getEntity());
479 break;
480 case DataOperation.QUERY:
481 break;
482 case DataOperation.STORE:
483 case DataOperation.REFRESH:
484 if(isTableCachable(operation.getEntity().getEntityDescriptor()))
485 refreshCachedEntity(operation.getEntity());
486 break;
487 case DataOperation.DELETE:
488 if(isTableCachable(operation.getEntity().getEntityDescriptor()))
489 deleteCachedEntity(operation.getEntity());
490 break;
491 }
492 }
493 }
494 catch(Exception e)
495 {
496 if(e instanceof DataServiceException)
497 throw (DataServiceException)e;
498 else
499 throw new DataServiceException("Transaction failed in CachedDataService", e);
500 }
501
502
503 mSourceTransaction = null;
504 }
505 }
506
507 public void abortTransaction() throws DataServiceException
508 {
509 if (mSourceTransaction != null)
510 mSourceTransaction.abortTransaction();
511 }
512
513
514 protected IDataTransaction getSourceTransaction()
515 {
516 if(mSourceTransaction==null)
517 mSourceTransaction = mSourceDataService.newTransaction();
518 return mSourceTransaction;
519 }
520 }
521
522 protected class CacheEntry
523 {
524
525 private long mEntryTime;
526 private Object mObject;
527
528
529
530 public CacheEntry(IEntity entity)
531 {
532 mEntryTime = System.currentTimeMillis();
533 mObject = entity;
534 }
535
536 public CacheEntry(ISelection selection)
537 {
538 mEntryTime = System.currentTimeMillis();
539 mObject = selection;
540 }
541
542
543
544 public IEntity getEntity()
545 {
546 return (IEntity)mObject;
547 }
548
549 public ISelection getSelection()
550 {
551 return (ISelection)mObject;
552 }
553
554 public boolean isValid()
555 {
556 long cacheTime = System.currentTimeMillis()-mEntryTime;
557 if(mObject instanceof IEntity && ((IEntity)mObject).getEntityDescriptor().getCacheTime()>=sMinimumEntityCacheTime)
558 return ((IEntity)mObject).getEntityDescriptor().getCacheTime()*1000 > cacheTime;
559 else if(mObject instanceof ISelection && ((ISelection)mObject).getEntityDescriptor().getCacheTime()>=sMinimumTableCacheTime)
560 return ((ISelection)mObject).getEntityDescriptor().getCacheTime()*1000 > cacheTime;
561 else
562 return false;
563 }
564 }
565 }