1 /*****************************************************************************/
2 /* Software Testing Automation Framework (STAF)                              */
3 /* (C) Copyright IBM Corp. 2002, 2007                                        */
4 /*                                                                           */
5 /* This software is licensed under the Eclipse Public License (EPL) V1.0.    */
6 /*****************************************************************************/
7 
8 package com.ibm.staf.service.stax;
9 
10 import java.net.InetAddress;
11 import java.net.UnknownHostException;
12 import java.util.Calendar;
13 import java.util.GregorianCalendar;
14 import java.util.Comparator;
15 import java.util.Collections;
16 import java.util.Date;
17 import java.util.HashMap;
18 import java.util.HashSet;
19 import java.util.Iterator;
20 import java.util.LinkedList;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24 
25 import com.ibm.staf.STAFResult;
26 import com.ibm.staf.STAFUtil;
27 import com.ibm.staf.STAFHandle;
28 
29 /**
30  * Caches STAX XML files.
31  */
32 public class STAXFileCache
33 {
34     private static final int DEFAULT_CACHE_SIZE = 20;
35 
36     /**
37      *  LRU is the "Least Recently Used" cache replacement algorithm.
38      *  If a new entry needs to be added to the cache and the cache is full,
39      *  the least recently used cache entry will be removed.
40      */
41     public static final int LRU = 1;
42     private static final String LRU_STRING = "LRU";
43 
44     /**
45      *  LFU is the "Least Frequently Used" cache replacement algorithm
46      *  that can also be configured to get rid of the oldest "stale" entry.
47      *  If a new entry needs to be added to the cache and the cache is full,
48      *  - If maxAge = 0 (the default indicating no entries are considered to
49      *    be stale), the least frequently used cache entry will be removed
50      *  - if maxAge > 0, if any cache entry is stale, the mleast recently used
51      *    stale entry will be removed.  Otherwise, if no cache entries are
52      *    stale, the least frequently used cache entry will be removed.
53      */
54     public static final int LFU = 2;
55     private static final String LFU_STRING = "LFU";
56 
57     private static STAXFileCache instance;
58 
59     /**
60      * A map contain the cache entries
61      */
62     private Map<CacheKey, FileCacheEntry> cache;
63 
64     /**
65      * Maximum number of entries that will be stroed in the cache
66      */
67     private int maxCacheSize;
68 
69     /**
70      * Caching algorithm (LRU=1 or LFU=2)
71      */
72     private int algorithm;
73 
74     /**
75      *  Maximum age before a cached document is considered "stale"
76      * if using the LFU algorithm.  0 indicates no maximum age.
77      */
78     private MaxAge maxAge;
79 
80     /**
81      *  The number of times a cache hit has occurred for any item in the cache
82      */
83     private long cacheHitCount = 0;
84 
85     /**
86      * The number of times a cache miss has occurred for any item not in cache
87      * or that was stale (e.g. not up-to-date)
88      */
89     private long cacheMissCount = 0;
90 
91     /**
92      * Date that the cache was initialized or last purged
93      */
94     private Date lastPurgeDate;
95 
96     private Set<String> localMachineNames;
97 
98     /**
99      * Initializes the STAXCache instance.
100      */
STAXFileCache()101     private STAXFileCache()
102     {
103         maxCacheSize = DEFAULT_CACHE_SIZE;
104         algorithm = LRU;  // Default cache algorithm
105         maxAge = new MaxAge(); // The default is 0 = No maximum age
106         lastPurgeDate = new Date();
107         cache = Collections.synchronizedMap(
108             new HashMap<CacheKey, FileCacheEntry>());
109 
110         localMachineNames = new HashSet<String>();
111         addLocalMachineNames();
112     }
113 
114     /**
115      * Adds known local machine name aliases
116      */
addLocalMachineNames()117     private void addLocalMachineNames()
118     {
119         localMachineNames.add("local");
120         localMachineNames.add("localhost");
121         localMachineNames.add("127.0.0.1");
122 
123         try
124         {
125             InetAddress addr = InetAddress.getLocalHost();
126 
127             localMachineNames.add(addr.getHostAddress().toLowerCase());
128             localMachineNames.add(addr.getHostName().toLowerCase());
129             localMachineNames.add(addr.getCanonicalHostName().toLowerCase());
130         }
131         catch (UnknownHostException e)
132         {
133             // Do nothing
134         }
135     }
136 
137     /**
138      * Adds a known local machine name alias.
139      *
140      * @param name a local machine name alias such as "localhost".
141      */
addLocalMachineName(String name)142     public void addLocalMachineName(String name)
143     {
144         localMachineNames.add(name.toLowerCase());
145     }
146 
147     /**
148      * Gets the cache instance.
149      *
150      * @return the STAX document cache.
151      */
get()152     public static STAXFileCache get()
153     {
154         if (instance == null)
155         {
156             instance = new STAXFileCache();
157         }
158 
159         return instance;
160     }
161 
162     /**
163      * Get the file separator for the operating system of the specified
164      * machine.
165      *
166      * @param machine A string containing the endpoint of a machine
167      * @param handle  A STAFHandle object to use to submit the VAR RESOLVE
168      *                request
169      * @return STAFResult If successful, rc = 0 and result contains the file
170      * separator.  If fails, rc = non-zero and result contains an error
171      * message.
172      */
getFileSep(String machine, STAFHandle handle)173     public static STAFResult getFileSep(String machine, STAFHandle handle)
174     {
175         String fileSep = "";
176 
177         if (STAXFileCache.get().isLocalMachine(machine))
178         {
179             // Assign the file separator for the local STAX machine
180             fileSep = STAX.fileSep;
181             return new STAFResult(STAFResult.Ok, fileSep);
182         }
183 
184         // Get the file separator for the remote machine.
185 
186         // Get the file separator from the machine cache if the remote
187         // machine is in the cache (so that don't have to submit a VAR
188         // RESOLVE request to the remote machine).
189 
190         if (STAXMachineCache.get().checkCache(machine))
191         {
192             fileSep = STAXMachineCache.get().getFileSep(machine);
193             return new STAFResult(STAFResult.Ok, fileSep);
194         }
195 
196         // The remote machine is not in the machine cache, so submit a
197         // VAR RESOLVE request to get the file separator and add the
198         // machine/fileSep to the machine cache.
199 
200         STAFResult result = handle.submit2(
201             machine, "VAR", "RESOLVE STRING {STAF/Config/Sep/File}");
202 
203         if (result.rc == STAFResult.Ok)
204         {
205             fileSep = result.result;
206 
207             // Add the machine/fileSep to the machine cache
208             STAXMachineCache.get().addMachine(machine, fileSep);
209         }
210 
211         return result;
212     }
213 
214 
215     /**
216      * Checks if the given machine name is a known alias for
217      * the local machine.
218      *
219      * @param machine the machine name to check.
220      * @return whether the machine is known to be an alias to the
221      * local machine.
222      */
isLocalMachine(String machine)223     public boolean isLocalMachine(String machine)
224     {
225         return localMachineNames.contains(formatEndpoint(machine));
226     }
227 
228     /**
229      * Formats a machine endpoint. This takes a STAF endpoint in the
230      * format [&lt;Interface>://]<Logical or Physical Identifier>[@&lt;Port>]
231      * and strips off the interface and port leaving only the logical
232      * or physical identifier. The identifier will also be converterd to
233      * lowercase.
234      *
235      * @param machine the machine (STAF endpoint) name to format.
236      * @return the logical or physical identifier of the endpoint.
237      */
formatEndpoint(String endpoint)238     public static String formatEndpoint(String endpoint)
239     {
240         // Strip the port
241         endpoint = STAFUtil.stripPortFromEndpoint(endpoint);
242 
243         // Strip the interface
244         int index = endpoint.indexOf("://");
245         if (index > -1)
246         {
247             endpoint = endpoint.substring(index + 3);
248         }
249 
250         endpoint = endpoint.toLowerCase();
251 
252         return endpoint;
253     }
254 
255     /**
256      * Gets a STAX document from the cache. This does not check to see if the
257      * cached copy is up-to-date. The method checkCache should first be called
258      * (if necessary) to determine if the cached copy exists and is up-to-date.
259      *
260      * @param machine the machine containg the XML file.
261      * @param filename the STAX XML file name.
262      * @param caseSensitiveFileName a flag indicating if the filename is
263      *                              case-sensitive
264      * @return a cached document or null if the document is not in the cache.
265      */
getDocument(String machine, String filename, boolean caseSensitiveFileName)266     public STAXDocument getDocument(String machine, String filename,
267                                     boolean caseSensitiveFileName)
268     {
269         STAXDocument doc = null;
270 
271         machine = formatEndpoint(machine);
272 
273         if (isLocalMachine(machine))
274         {
275             machine = "local";
276         }
277 
278         FileCacheEntry item = cache.get(
279             new CacheKey(machine, filename, caseSensitiveFileName));
280 
281         if (item != null)
282         {
283             doc = item.getDocument().cloneDocument();
284             item.hit();
285             cacheHitCount++;
286         }
287 
288         return doc;
289     }
290 
291     /**
292      * Gets the contents of the cache in the sort order specified.
293      *
294      * @return a sorted list of STAXCache.CachedSTAX items.
295      */
getCacheContents(int sortBy)296     public List<FileCacheEntry> getCacheContents(int sortBy)
297     {
298         List<FileCacheEntry> cacheContents = null;
299 
300         synchronized(cache)
301         {
302             cacheContents = new LinkedList<FileCacheEntry>(cache.values());
303         }
304 
305         if (sortBy == STAXFileCache.LRU)
306         {
307             // Sort the contents by the last hit date in descending order
308 
309             Collections.sort(
310                 cacheContents,
311                 new Comparator<STAXFileCache.FileCacheEntry>()
312             {
313                 public int compare(STAXFileCache.FileCacheEntry entry1,
314                                    STAXFileCache.FileCacheEntry entry2)
315                 {
316                     // Sort by most recent hit date first
317                     return entry1.getLastHitDate().compareTo(
318                         entry2.getLastHitDate()) * -1;
319                 }
320             });
321         }
322         else if (sortBy == STAXFileCache.LFU)
323         {
324             // Sort the contents as follows:
325             // - Sort first by whether stale or not.  Put the non-stale
326             //   documents before all stale documents.  Sort the non-stale
327             //   documents by the greatest number of hits, and if the same
328             //   number of hits, then by recent hit date first.
329             // - Sort the stale documents by the most recent hit date first.
330 
331             Collections.sort(
332                 cacheContents,
333                 new Comparator<STAXFileCache.FileCacheEntry>()
334             {
335                 public int compare(STAXFileCache.FileCacheEntry e1,
336                                    STAXFileCache.FileCacheEntry e2)
337                 {
338                     boolean e1_isStale = isStale(e1.getLastHitDate());
339                     boolean e2_isStale = isStale(e2.getLastHitDate());
340 
341                     if (e1_isStale == false && e2_isStale == false)
342                     {
343                         // Both entries are not stale.
344                         // Sort by greatest number of hits first
345 
346                         int compareResult = (new Long(e1.getHits())).
347                             compareTo(new Long(e2.getHits()));
348 
349                         if (compareResult == 0)
350                         {
351                             // Same number of hits
352 
353                             return e1.getLastHitDate().compareTo(
354                                 e2.getLastHitDate()) * -1;
355                         }
356                         else
357                         {
358                             return compareResult * -1;
359                         }
360                     }
361                     else if (e1_isStale == true && e2_isStale == true)
362                     {
363                         // Both entries are stale.
364                         // Sort by recent hit date first.
365 
366                         return e1.getLastHitDate().compareTo(
367                             e2.getLastHitDate()) * -1;
368                     }
369                     else if (e1_isStale == false)
370                     {
371                         // e1_isStale == false && e2_isStale == true
372                         return -1;
373                     }
374                     else
375                     {
376                         // e1_isStale == true && e2_isStale == false
377                         return 1;
378                     }
379                 }
380             });
381         }
382 
383         return cacheContents;
384     }
385 
386     /**
387      * Clears the contents of the cache and clears the cache statistics.
388      *
389      * @return the number of files cleared from the cache.
390      */
purge()391     public int purge()
392     {
393         synchronized(cache)
394         {
395             int cleared = cache.size();
396             cache.clear();
397             lastPurgeDate = new Date();
398 
399             // Clear the cache statistics
400             cacheHitCount = 0;
401             cacheMissCount = 0;
402 
403             return cleared;
404         }
405     }
406 
407     /**
408      * Converts a STAX date string to a java Date object.
409      *
410      * @param staxDate the STAX date string.
411      * @return a java Date object.
412      */
convertSTAXDate(String staxDate)413     public static Date convertSTAXDate(String staxDate)
414     {
415         String sYear = staxDate.substring(0,4);
416         String sMonth = staxDate.substring(4,4+2);
417         String sDay = staxDate.substring(6,6+2);
418         String sHour = staxDate.substring(9,9+2);
419         String sMinute = staxDate.substring(12,12+2);
420         String sSecond = staxDate.substring(15,15+2);
421 
422         int year = Integer.parseInt(sYear);
423 
424         // Calendar expects month to be zero-based (0 = January)
425         int month = Integer.parseInt(sMonth) - 1;
426         int day = Integer.parseInt(sDay);
427         int hour = Integer.parseInt(sHour);
428         int minute = Integer.parseInt(sMinute);
429         int second = Integer.parseInt(sSecond);
430 
431         Calendar cal = Calendar.getInstance();
432         cal.set(year, month, day, hour, minute, second);
433 
434         // Chop off the milliseconds portion
435         // (Should be zero but Calendar sets it to the current system
436         // milliseconds)
437         long time = ((long)(cal.getTimeInMillis() / 1000))*1000;
438 
439         return new Date(time);
440     }
441 
442     /**
443      * Checks the cache to see if it contains an up-to-date copy of a
444      * STAX document.
445      *
446      * @param machine the machine that contains the STAX XML.
447      * @param filename the filename of the STAX XML.
448      * @param date the current last modification date of the file.
449      * @param caseSensitiveFileName a flag indicating if the filename is
450      *                              case-sensitive
451      * @return true if the item is cached, false if the item is not cached or
452      *             the cached copy is old.
453      */
checkCache(String machine, String filename, Date date, boolean caseSensitiveFileName)454     public boolean checkCache(String machine, String filename, Date date,
455                               boolean caseSensitiveFileName)
456     {
457         machine = formatEndpoint(machine);
458 
459         if (isLocalMachine(machine))
460         {
461             machine = "local";
462         }
463 
464         FileCacheEntry item = cache.get(
465             new CacheKey(machine, filename, caseSensitiveFileName));
466 
467         if (item != null)
468         {
469             // Make sure the modification dates are the same, if not the
470             // cache is old.
471 
472             if (date.equals(item.getModDate()))
473             {
474                 return true;
475             }
476         }
477 
478         return false;
479     }
480 
481     /**
482      * Adds a STAX document to the cache, or if it already exists then
483      * that means that the document's modification date has changed, so
484      * then it updates the cache entry with the new document, its new
485      * modification date, and its last hit date.
486      *
487      * @param filename the file name of the STAX xml file.
488      * @param doc the parsed STAX document.
489      * @param modDate the modification date of the STAX file.
490      * @param caseSensitiveFileName a flag indicating if the filename is
491      *                              case-sensitive
492      */
addDocument(String machine, String filename, STAXDocument doc, Date modDate, boolean caseSensitiveFileName)493     public void addDocument(String machine, String filename, STAXDocument doc,
494                             Date modDate, boolean caseSensitiveFileName)
495     {
496         machine = formatEndpoint(machine);
497 
498         if (isLocalMachine(machine))
499         {
500             machine = "local";
501         }
502 
503         cacheMissCount++;
504 
505         // Don't add the document if the cache is off
506 
507         if (maxCacheSize == 0 )
508         {
509             return;
510         }
511 
512         CacheKey key = new CacheKey(machine, filename, caseSensitiveFileName);
513         FileCacheEntry item = null;
514         STAXDocument clonedDoc = doc.cloneDocument();
515 
516         synchronized(cache)
517         {
518             item = cache.get(key);
519 
520             if (item == null)
521             {
522                 // Add document to the cache
523 
524                 item = new FileCacheEntry(
525                     machine, filename, clonedDoc, modDate,
526                     caseSensitiveFileName);
527 
528                 // If the cache size isn't unlimited (-1) and the cache is
529                 // full, remove an entry from the cache to make room for
530                 // this new entry
531 
532                 if ((maxCacheSize != -1) && (cache.size() >= maxCacheSize))
533                 {
534                     while(cache.size() >= maxCacheSize)
535                     {
536                         if (algorithm == LRU)
537                             removeOldest();
538                         else
539                             removeLfu();
540                     }
541                 }
542             }
543             else
544             {
545                 // Already in cache, so update the document, the document's
546                 // modification date, and the date the document was added/
547                 // updated in the cache, and the last hit date.
548 
549                 item.setDocument(clonedDoc);
550                 item.setModDate(modDate);
551                 item.setLastHitDate(new Date());
552             }
553 
554             // Add or update the cache entry
555 
556             cache.put(key, item);
557         }
558     }
559 
560     /**
561      * Removes extra items from the cache when the STAX_CACHE_SIZE setting
562      * changes.
563      */
cleanup()564     public void cleanup()
565     {
566         // If the cache size isn't unlimited (-1) and the number of entries
567         // in the cache exceeds the maximum cache size (e.g. because the
568         // maximum cache size was just decreased), remove entries from the
569         // cache so that its total number of entries does not exceed the
570         // maximum size.
571 
572         if ((maxCacheSize != -1) && (cache.size() > maxCacheSize))
573         {
574             synchronized(cache)
575             {
576                 while(cache.size() > maxCacheSize)
577                 {
578                     if (algorithm == LRU)
579                         removeOldest();
580                     else
581                         removeLfu();
582                 }
583             }
584         }
585     }
586 
587     /**
588      * Removes the oldest entry in the cache.
589      *
590      * Must call when synchronized on the cache object.
591      */
removeOldest()592     private void removeOldest()
593     {
594         Iterator<FileCacheEntry> iter = cache.values().iterator();
595 
596         if (!iter.hasNext())
597         {
598             // No items in the cache
599             return;
600         }
601 
602         FileCacheEntry oldest = iter.next();
603 
604         while(iter.hasNext())
605         {
606             FileCacheEntry cached = iter.next();
607 
608             if (cached.getLastHitDate().before(oldest.getLastHitDate()))
609             {
610                 oldest = cached;
611             }
612         }
613 
614         cache.remove(oldest.getKey());
615     }
616 
617     /**
618      * Removes the entry with the least number of hits, the Least
619      * Frequently Used entry in the cache, unless there is a "stale"
620      * document in the cache (one that hasn't been accessed within the
621      * maxAge time period, assuming maxAge is not 0 which means that
622      * no documents will be considered to be stale).  Then, the most
623      * "stale" document in the cache will be removed instead.
624      *
625      * Must call when synchronized on the cache object.
626      */
removeLfu()627     private void removeLfu()
628     {
629         // Find the entry with the least number of hits and remove it
630 
631         Iterator<FileCacheEntry> iter = cache.values().iterator();
632 
633         if (!iter.hasNext())
634         {
635             // No items in the cache
636             return;
637         }
638 
639         FileCacheEntry lowest = iter.next();
640         FileCacheEntry oldest = lowest;
641 
642         while(iter.hasNext())
643         {
644             FileCacheEntry cached = iter.next();
645 
646             if (maxAge.getAmount() != 0)
647             {
648                 // A maximum age has been specified so determine the
649                 // oldest entry in the cache
650 
651                 if (cached.getLastHitDate().before(oldest.getLastHitDate()))
652                 {
653                     oldest = cached;
654                 }
655             }
656 
657             // Determine the entry with the lowest number of hits and
658             // the least recent hit date
659 
660             if (cached.getHits() < lowest.getHits())
661             {
662                 lowest = cached;
663             }
664             else if (cached.getHits() == lowest.getHits())
665             {
666                 // Set to the entry that is oldest
667 
668                 if (cached.getLastHitDate().before(lowest.getLastHitDate()))
669                 {
670                     lowest = cached;
671                 }
672             }
673         }
674 
675         // If a maximum age has been configured (e.g. != 0), check if the
676         // document with the oldest last hit date is stale and if so,
677         // remove it and return
678 
679         if (isStale(oldest.getLastHitDate()))
680         {
681             cache.remove(oldest.getKey());
682 
683             return;
684         }
685 
686         // Otherwise, remove the document with the least number of hits
687         // (the Least Frequently Used entry)
688 
689         cache.remove(lowest.getKey());
690     }
691 
692     /**
693      * Check if a document is stale by checking if a document's last hit
694      * timestamp plus the maxAge is earlier than the current timestamp.
695      */
isStale(Date lastHitDate)696     private boolean isStale(Date lastHitDate)
697     {
698         // A maxAge amount of 0 means no maximum age, so no document will
699         // be considered to be stale
700 
701         if (maxAge.getAmount() == 0)
702             return false;
703 
704         Calendar calendar = new GregorianCalendar();
705 
706         // Tell the calendar to the last hit date/time passed in
707         calendar.setTime(lastHitDate);
708 
709         // Add the time it takes before a document is considered stale to the
710         // last hit date
711 
712         calendar.add(maxAge.getTimeField(), maxAge.getAmount());
713 
714         // Check if the last hit timestamp + maxAge is earlier than the
715         // current date/time
716 
717         return calendar.before(Calendar.getInstance());
718     }
719 
720     /**
721      * Sets the maximum number of STAX files to cache.
722      *
723      * @param size the maximum STAX files that will be stored in cache.
724      */
setMaxCacheSize(int size)725     public void setMaxCacheSize(int size)
726     {
727         this.maxCacheSize = size;
728         cleanup();
729     }
730 
731     /**
732      * Gets the maximum number of STAX files that can be held in cache.
733      *
734      * @return the maximum cache size.
735      */
getMaxCacheSize()736     public int getMaxCacheSize()
737     {
738         return this.maxCacheSize;
739     }
740 
741     /**
742      * Sets the cache algorithm used to determine the entry to be
743      * removed when the cache is full.
744      *
745      * @return A STAFResult object with rc 0 if the algorithm was
746      *         set successfully.  If an invalid algorithm is
747      *         specified, its rc will be set to a non-zero value
748      *         with an error message in its result.
749      */
setAlgorithm(String algorithm)750     public STAFResult setAlgorithm(String algorithm)
751     {
752         if (algorithm.equalsIgnoreCase(LRU_STRING))
753         {
754             this.algorithm = LRU;
755         }
756         else if (algorithm.equalsIgnoreCase(LFU_STRING))
757         {
758             this.algorithm = LFU;
759         }
760         else
761         {
762             return new STAFResult(
763                 STAFResult.InvalidValue,
764                 "FILECACHEALGORITHM must be set to LRU or LFU.  " +
765                 "Invalid value: " + algorithm);
766         }
767 
768         return new STAFResult(STAFResult.Ok);
769     }
770 
771     /**
772      * Gets the cache algorithm used to determine the entry to be
773      * removed when the cache is full.
774      *
775      * @return the cache algorithm.
776      */
getAlgorithm()777     public int getAlgorithm()
778     {
779         return this.algorithm;
780     }
781 
782     /**
783      * Gets the cache algorithm in its String form
784      *
785      * @return the cache algorithm's name.
786      */
getAlgorithmString()787     public String getAlgorithmString()
788     {
789         if (this.algorithm == LRU)
790             return LRU_STRING;
791         else
792             return LFU_STRING;
793     }
794 
795     /**
796      * Sets the maximum age for cached STAX documents.
797      * This is used only when the cache algorithm is LFU to determine
798      * if a document is stale.
799      *
800      * @param age the maximum age for a cached document (e.g. "30",
801      * "30s", "5m", "2h", "1d", "1w").
802      */
setMaxAge(String maxAge)803     public STAFResult setMaxAge(String maxAge)
804     {
805         try
806         {
807             this.maxAge = new MaxAge(maxAge);
808         }
809         catch (Exception e)
810         {
811             return new STAFResult(STAFResult.InvalidValue, e.getMessage());
812         }
813 
814         return new STAFResult(STAFResult.Ok);
815     }
816 
817     /**
818      * Gets the maximum age for cached STAX documents.
819      *
820      * @return the maximum age.
821      */
getMaxAge()822     public MaxAge getMaxAge()
823     {
824         return this.maxAge;
825     }
826 
getCacheHitCount()827     public long getCacheHitCount()
828     {
829         return this.cacheHitCount;
830     }
831 
getCacheMissCount()832     public long getCacheMissCount()
833     {
834         return this.cacheMissCount;
835     }
836 
getLastPurgeDate()837     public Date getLastPurgeDate()
838     {
839         return this.lastPurgeDate;
840     }
841 
842     /**
843      * This class's constructor sets its time field and amount field
844      * based on the maxAge string that is passed in the format of
845      * <Number>[s|m|h|d|w] so that it can be used to "add" the time
846      * to the last hit date to determine if a document is stale.
847      */
848     private static class MaxAge
849     {
850         private static String sMAX_AGE_ERROR_MSG =
851             "The MAXFILECACHEAGE value may be expressed in seconds, " +
852             "minutes, hours, days, or weeks.  Its format is " +
853             "<Number>[s|m|h|d|w] where <Number> must be an integer from 0 " +
854             "to 2147483647 and indicates seconds unless one of the " +
855             "following case-insensitive suffixes is specified:  " +
856             "s (for seconds), m (for minutes), h (for hours), " +
857             "d (for days), or w (for weeks).  If &lt;Number> is 0, this " +
858             "means that there is no maximum age.\n\nExamples: \n" +
859             "  0 specifies no maximum age, \n" +
860             "  30 specifies 30 seconds, \n" +
861             "  30s specifies 30 seconds, \n" +
862             "  5m specifies 5 minutes, \n" +
863             "  2h specifies 2 hours, \n" +
864             "  1d specifies 1 day, \n" +
865             "  1w specifies 1 week.";
866 
867         /**
868          * The value represented as a string (e.g. "30", "30s", "1m", "2h",
869          * "1d", "1w")
870          */
871         private String displayString = "0";
872 
873         /**
874          * The time field (e.g. Calendar.SECOND, Calendar.MINUTE,
875          * Calendar.HOUR_OF_DAY, Calendar.Date, or Calendar.WEEK_OF_YEAR
876          */
877         private int timeField = Calendar.SECOND;
878 
879         /**
880          *  The amount of date or time to be added
881          */
882         private int amount = 0;
883 
MaxAge()884         private MaxAge()
885         {
886             // Do nothing.  Sets to 0 which means No maximum age
887         }
888 
MaxAge(String maxAge)889         public MaxAge(String maxAge) throws Exception
890         {
891             maxAge = maxAge.trim();
892 
893             if ((maxAge == null) || (maxAge.length() == 0))
894                 throw new Exception(sMAX_AGE_ERROR_MSG);
895 
896             displayString = maxAge;
897 
898             // Check if the max age string is not all digits
899 
900             try
901             {
902                 amount = (new Integer(maxAge)).intValue();
903             }
904             catch (NumberFormatException e)
905             {
906                 // Get max age type (last character of max age string)
907 
908                 String maxAgeType = maxAge.substring(
909                     maxAge.length() - 1).toLowerCase();
910 
911                 if (maxAgeType.equals("s"))
912                     timeField = Calendar.SECOND;
913                 else if (maxAgeType.equals("m"))
914                     timeField = Calendar.MINUTE;
915                 else if (maxAgeType.equals("h"))
916                     timeField = Calendar.HOUR_OF_DAY;
917                 else if (maxAgeType.equals("d"))
918                     timeField = Calendar.DATE;
919                 else if (maxAgeType.equals("w"))
920                     timeField = Calendar.WEEK_OF_YEAR;
921                 else
922                     throw new Exception(sMAX_AGE_ERROR_MSG);
923 
924                 // Assign numeric max age (all characters except the last one)
925 
926                 try
927                 {
928                     amount = (new Integer(
929                         maxAge.substring(0, maxAge.length() - 1))).intValue();
930                 }
931                 catch (NumberFormatException e2)
932                 {
933                     throw new Exception(sMAX_AGE_ERROR_MSG);
934                 }
935             }
936 
937             if (amount < 0)
938                 throw new Exception(sMAX_AGE_ERROR_MSG);
939         }
940 
toString()941         public String toString()
942         {
943             return displayString;
944         }
945 
getTimeField()946         public int getTimeField()
947         {
948             return timeField;
949         }
950 
getAmount()951         public int getAmount()
952         {
953             return amount;
954         }
955     }
956 
957     private static class CacheKey
958     {
959         private String machine;
960         private String filename;
961 
CacheKey(String machine, String filename, boolean caseSensitiveFileName)962         public CacheKey(String machine, String filename,
963                         boolean caseSensitiveFileName)
964         {
965             this.machine = machine.toLowerCase();
966 
967             if (caseSensitiveFileName)
968                 this.filename = filename;
969             else
970                 this.filename = filename.toLowerCase();
971         }
972 
getMachine()973         public String getMachine()
974         {
975             return machine;
976         }
977 
getFilename()978         public String getFilename()
979         {
980             return filename;
981         }
982 
983         /* (non-Javadoc)
984          * @see java.lang.Object#equals(java.lang.Object)
985          */
equals(Object o)986         public boolean equals(Object o)
987         {
988             if (this == o)
989             {
990                 return true;
991             }
992 
993             if (o instanceof CacheKey)
994             {
995                 CacheKey k = (CacheKey)o;
996 
997                 return getMachine().equals(k.getMachine()) &&
998                     getFilename().equals(k.getFilename());
999             }
1000 
1001             return false;
1002         }
1003 
1004         /* (non-Javadoc)
1005          * @see java.lang.Object#hashCode()
1006          */
hashCode()1007         public int hashCode()
1008         {
1009             return machine.hashCode() + filename.hashCode();
1010         }
1011 
1012     }
1013 
1014     /**
1015      * A Cached STAX document.
1016      */
1017     public static class FileCacheEntry
1018     {
1019         private String machine;
1020         private String filename;
1021         private boolean caseSensitiveFileName = true;
1022 
1023         private Date date;
1024         private Date modDate;
1025         private Date lastHit;
1026         private long hits;
1027         private STAXDocument doc;
1028 
1029         private CacheKey key;
1030 
1031         /**
1032          * Creates a cached document.
1033          *
1034          * @param doc the document to cache.
1035          */
FileCacheEntry(String machine, String filename, STAXDocument doc, Date modDate, boolean caseSensitiveFileName)1036         public FileCacheEntry(String machine, String filename,
1037                               STAXDocument doc, Date modDate,
1038                               boolean caseSensitiveFileName)
1039         {
1040             this.machine = machine;
1041             this.filename = filename;
1042             this.caseSensitiveFileName = caseSensitiveFileName;
1043             this.doc = doc;
1044             this.modDate = modDate;
1045             this.date = new Date();
1046             this.lastHit = new Date();
1047             this.hits = 0;
1048             this.key = new CacheKey(machine, filename, caseSensitiveFileName);
1049         }
1050 
1051         /**
1052          * Gets the key used to cache this entry.
1053          *
1054          * @return the cache key for this entry.
1055          */
getKey()1056         public CacheKey getKey()
1057         {
1058             return key;
1059         }
1060 
1061         /**
1062          * Gets the name of the machine that provided the file.
1063          *
1064          * @return the machine name.
1065          */
getMachine()1066         public String getMachine()
1067         {
1068             return machine;
1069         }
1070 
1071         /**
1072          * Gets the name/path to the file as it exists on the providing
1073          * machine.
1074          *
1075          * @return the name/path to the file.
1076          */
getFilename()1077         public String getFilename()
1078         {
1079             return filename;
1080         }
1081 
1082         /**
1083          * Gets a flag indicating if file names are case-sensitive
1084          *
1085          * @return true if the file name is case-sensitive; false if not
1086          * case-sensitive.
1087          */
getCaseSensitive()1088         public boolean getCaseSensitive()
1089         {
1090             return caseSensitiveFileName;
1091         }
1092 
1093         /**
1094          * Gets the STAX document.
1095          *
1096          * @return the cached STAX document.
1097          */
getDocument()1098         public STAXDocument getDocument()
1099         {
1100             return doc;
1101         }
1102 
1103         /**
1104          * Sets the STAX document.
1105          */
setDocument(STAXDocument doc)1106         public void setDocument(STAXDocument doc)
1107         {
1108             this.doc = doc;
1109         }
1110 
1111         /**
1112          * Updates the cache hit for this document.
1113          */
hit()1114         public void hit()
1115         {
1116             lastHit = new Date();
1117             hits++;
1118         }
1119 
1120         /**
1121          * Gets the number of cache hits for this document.
1122          *
1123          * @return the number of cache hits.
1124          */
getHits()1125         public long getHits()
1126         {
1127             return hits;
1128         }
1129 
1130         /**
1131          * Gets the date when the STAX file was last modified.
1132          *
1133          * @return
1134          */
getModDate()1135         public Date getModDate()
1136         {
1137             return modDate;
1138         }
1139 
1140         /**
1141          * Sets the date when the STAX file was last modified.
1142          */
setModDate(Date modDate)1143         public void setModDate(Date modDate)
1144         {
1145             this.modDate = modDate;
1146         }
1147 
1148         /**
1149          * Gets last time the document in the cache was accessed.
1150          *
1151          * @return the last cache hit time.
1152          */
getLastHitDate()1153         public Date getLastHitDate()
1154         {
1155             return lastHit;
1156         }
1157 
1158         /**
1159          * Sets the date when the document was last accessed in the
1160          * cache.
1161          */
setLastHitDate(Date lastHit)1162         public void setLastHitDate(Date lastHit)
1163         {
1164             this.lastHit = lastHit;
1165         }
1166 
1167         /**
1168          * Gets the date when this document was added to the cache.
1169          *
1170          * @return the date the document was added to the cache.
1171          */
getAddDate()1172         public Date getAddDate()
1173         {
1174             return date;
1175         }
1176 
1177         /* (non-Javadoc)
1178          * @see java.lang.Object#equals(java.lang.Object)
1179          */
equals(Object o)1180         public boolean equals(Object o)
1181         {
1182             if (this == o)
1183             {
1184                 return true;
1185             }
1186 
1187             if (o instanceof FileCacheEntry)
1188             {
1189                 FileCacheEntry o1 = (FileCacheEntry)o;
1190 
1191                 // Two documents are equivalent if they have the same machine,
1192                 // the same filename (case-insensitive if Windows and case-
1193                 // sensitive if Unix), and the same last modified date
1194 
1195                 boolean equals =
1196                     this.getMachine().equalsIgnoreCase(o1.getMachine()) &&
1197                     ((this.getCaseSensitive() &&
1198                       this.getFilename().equals(o1.getFilename())) ||
1199                      (!this.getCaseSensitive() &&
1200                       this.getFilename().equalsIgnoreCase(o1.getFilename()))) &&
1201                     this.getModDate().equals(o1.getModDate());
1202 
1203                 return equals;
1204             }
1205 
1206             return false;
1207         }
1208     }
1209 }
1210