View Javadoc

1   package com.atlassian.plugin.cache.filecache.impl;
2   
3   import com.atlassian.plugin.cache.filecache.FileCache;
4   import com.atlassian.plugin.cache.filecache.FileCacheStreamProvider;
5   import com.atlassian.plugin.servlet.DownloadException;
6   import com.google.common.base.Function;
7   import com.google.common.collect.MapEvictionListener;
8   import com.google.common.collect.MapMaker;
9   
10  import java.io.File;
11  import java.io.IOException;
12  import java.io.OutputStream;
13  import java.util.ArrayList;
14  import java.util.Collection;
15  import java.util.concurrent.ConcurrentMap;
16  import java.util.concurrent.atomic.AtomicLong;
17  
18  /**
19   * Implements a thread-safe FileCache.
20   * <p/>
21   * This implementation uses a computing map LRU map to create missing entries, and an eviction listener
22   * to identify files that need to be deleted.
23   * <p/>
24   * Concurrent control to the contents of each cache entry is implemented in {@link CachedFile}
25   *
26   * @since v2.13
27   */
28  public class FileCacheImpl<K> implements FileCache<K> {
29  
30      static final String EXT = ".cachedfile";
31  
32      private final ConcurrentMap<K, CachedFile> cache;
33      private final File tmpDir;
34      private final AtomicLong filenameCounter;
35  
36      /**
37       * @param tmpDir            directory to store cache contents. Files in this dir will be deleted by constructor
38       * @param maxSize           size of the LRU cache. zero means evict-immediately
39       * @param filenameCounter   counter for cache filenames. Passed in to allow multiple instances to share the same counter.
40       */
41      public FileCacheImpl(File tmpDir, int maxSize, AtomicLong filenameCounter) throws IOException {
42          if (maxSize < 0) {
43              throw new IllegalArgumentException("Max files can not be less than zero");
44          }
45          initDirectory(tmpDir);
46  
47          this.tmpDir = tmpDir;
48  
49          cache = new MapMaker()
50                  .maximumSize(maxSize)
51                  .evictionListener(new MapEvictionListener<K, CachedFile>() {
52                      @Override
53                      public void onEviction(K key, CachedFile value) {
54                          FileCacheImpl.this.onEviction(value);
55                      }
56                  })
57                  .makeComputingMap(new Function<K, CachedFile>() {
58                      @Override
59                      public CachedFile apply(K key) {
60                          return newCachedFile();
61                      }
62                  });
63  
64          this.filenameCounter = filenameCounter;
65      }
66  
67      private static void initDirectory(File tmpDir) throws IOException {
68          tmpDir.mkdirs();
69          File[] files = tmpDir.listFiles();
70          if (files != null) {
71              for (File f : files) {
72                  if (f.getName().toLowerCase().endsWith(EXT) && !f.delete()) {
73                      throw new IOException("Could not delete existing cache file " + f);
74                  }
75              }
76          }
77  
78          if (!tmpDir.isDirectory()) {
79              throw new IOException("Could not create tmp directory " + tmpDir);
80          }
81      }
82  
83      @Override
84      public void stream(K key, OutputStream dest, FileCacheStreamProvider input) throws DownloadException {
85          final CachedFile cachedFile = cache.get(key);
86          // Call stream() ASAP, even though `cachedFile` may already be evicted by the time we call it.
87          // That's okay, though, it still streams (just not from the cache).
88          cachedFile.stream(dest, input);
89      }
90  
91      @Override
92      public void clear() {
93          // betweel the call to .values() and .clear(), further cache entries may be added.
94          // In that case, we will never call .delete() on those files, and they will be orhpaned.
95          // That's not so bad.
96          
97          Collection<CachedFile> cachedFiles = new ArrayList<CachedFile>(cache.values());
98          cache.clear();
99          
100         for (CachedFile cachedFile : cachedFiles) {
101             cachedFile.deleteWhenPossible();
102         }
103     }
104 
105     private CachedFile newCachedFile() {
106         File file = new File(tmpDir, filenameCounter.incrementAndGet() + EXT);
107         return new CachedFile(file);
108     }
109 
110     private void onEviction(CachedFile node) {
111         node.deleteWhenPossible();
112     }
113 
114 }