View Javadoc

1   /* (c) Copyright 2003 Caleigo AB, All rights reserved. 
2    * 
3    * This library is free software; you can redistribute it and/or
4    * modify it under the terms of the GNU Lesser General Public
5    * License as published by the Free Software Foundation; either
6    * version 2.1 of the License, or (at your option) any later version.
7    * 
8    * This library is distributed in the hope that it will be useful,
9    * but WITHOUT ANY WARRANTY; without even the implied warranty of
10   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11   * Lesser General Public License for more details.
12   * 
13   * You should have received a copy of the GNU Lesser General Public
14   * License along with this library; if not, write to the Free Software
15   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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      // Constants ---------------------------------------------------------------
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      // Static data members -----------------------------------------------------
48      private static int sMinimumTableCacheTime = DEFAULT_MINIMUM_TABLE_CACHE_TIME;
49      private static int sMinimumEntityCacheTime = DEFAULT_MINIMUM_ENTITY_CACHE_TIME;
50  
51      // Data members ------------------------------------------------------------
52      protected final IDataService mSourceDataService;
53      
54      private Map mTableCache;
55      private LinkedList mEntityQueryList;
56  //    private LinkedList mSelectionQueryList;
57      
58      // Static methods ----------------------------------------------------------
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      // Constructors ------------------------------------------------------------
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  //        mSelectionQueryList = new LinkedList();
99      }
100 
101     // IDataService implementation ---------------------------------------------
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     // Action methods ----------------------------------------------------------
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         // Break if already cached or not cacheable.
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     // Access methods ----------------------------------------------------------
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         // Fetch cached table entry.
170         CacheEntry entry = (CacheEntry)mTableCache.get(entityDescriptor);
171         
172         // If entry found then validate it.
173         if(entry!=null && !entry.isValid())
174         {
175             // Remove entry if invalid.
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         // Fetch cached table entry.
192         CacheEntry entry = (CacheEntry)mTableCache.get(entityDescriptor);
193         
194         // If entry found then validate it.
195         if(entry!=null && !entry.isValid())
196         {
197             // Remove entry if invalid.
198             mTableCache.remove(entityDescriptor);
199             Log.print(this, "Removing outdated selection from table cache: "+entityDescriptor);
200             entry = null;
201         }
202 
203         // Cache table if not cached and if it is cacheable.
204         if(entry==null && isTableCachable(entityDescriptor))
205             this.cacheEntityTable(entityDescriptor);
206         
207         // Return cache entry if any.
208         entry = (CacheEntry)mTableCache.get(entityDescriptor);
209         if(entry!=null)
210             return entry.getSelection();
211         else
212             return null;
213     }
214     
215     // Help methods ------------------------------------------------------------
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             // Reset entity if data READ access is not granted.
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 //            Log.print(this, "Loading cached entity: "+targetEntity);
245         }
246 //        else
247 //            Log.print(this, "Loading cached entity: "+targetEntity.getEntityDescriptor()+" (Not Found)");
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                     // Store the entity if data READ access access is granted.
261                     // The entities should realy be copied here!!
262                     if(DataAccessManager.getManager().hasReadAccess(selection.getEntity(j)))
263                         targetSelection.addEntity(selection.getEntity(j));
264                 }
265 //            Log.print(this, "Loading cached selection: "+entityDescriptor+" ("+targetSelection.size()+"/"+selection.size()+" entities loaded)");
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         // Check entity validity.
283         if(entity.isEmpty() || !entity.isPersistent())
284             return;
285         
286         // Get cached table selection.
287         ISelection selection = this.getCachedEntityTable(entity.getEntityDescriptor());
288         if(selection==null)
289             Log.printWarning(this, "Data cache refresh inconsistency for entity: "+entity);
290 
291         // Find entity to update in selection.
292         int index = selection!=null ? selection.indexOf(entity) : -1;
293         if(index>=0)
294         {
295             // We should handle dirty entries specially, but how?!!
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(); // Remove outdated entries.
341                 else if(entry.getEntity().getEntityDescriptor()==targetEntity.getEntityDescriptor() && qualifier.doesQualify(entry.getEntity()))
342                 {
343                     // Reset entity if data READ access is not granted.
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 //                    Log.print(this, "Loading recent entity: "+targetEntity);
355                     
356                     // Move the enty to the front so that the most adressed 
357                     // entries allways are place in the start of the entry list. 
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     // Nested classes ----------------------------------------------------------
386     protected class CachedDataTransaction extends AbstractDataTransaction
387     {
388         // Data members --------------------------------------------------------
389         private IDataTransaction mSourceTransaction;
390         
391         // Constructors --------------------------------------------------------
392         public CachedDataTransaction()
393         {
394             super(getTimeout());
395         }
396         
397         // IDataService implementation -----------------------------------------
398         public void commit() throws DataServiceException
399         {
400             // Get data operation enumerator.
401             Enumeration dataOperations = this.getOperations();	
402             
403             // Execute all operations
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             // Commit and process source transaction if neaded.
436             if(mSourceTransaction!=null)
437             {
438                 try
439                 {
440                     // Commit the source transaction.
441                     if (this.getJob() != null)
442                     {
443                         // An asynch commit is going on
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                     // Process answers.
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                 // Reset the transaction.
503                 mSourceTransaction = null;
504             }
505         }
506         
507         public void abortTransaction() throws DataServiceException
508         {
509             if (mSourceTransaction != null)
510                 mSourceTransaction.abortTransaction();
511         }
512                 
513         // Help members --------------------------------------------------------
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         // Data members --------------------------------------------------------
525         private long mEntryTime;
526         private Object mObject;
527         
528         // Constructors --------------------------------------------------------
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         // Access methods ------------------------------------------------------
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 }