View Javadoc

1   package com.atlassian.plugin.cache.filecache.impl;
2   
3   import com.atlassian.plugin.cache.filecache.FileCacheStreamProvider;
4   import com.atlassian.plugin.servlet.DownloadException;
5   import org.apache.commons.io.IOUtils;
6   import org.slf4j.Logger;
7   import org.slf4j.LoggerFactory;
8   
9   import java.io.BufferedOutputStream;
10  import java.io.File;
11  import java.io.FileInputStream;
12  import java.io.FileOutputStream;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.io.OutputStream;
16  
17  /**
18   * Caches the contents of a stream, and deletes the cache when asked to do so.
19   * This class encapsulates the lifecycle of a cached file, from as-yet-uncached, to cached, to needs-deletion, to deleted.
20   *
21   * <p>Calls to stream() always succeed, even if the cache was deleted.
22   * The initial call to stream() will block other callers to stream() and deleteWhenPossible().
23   * <p>
24   * Subsequent calls to stream() don't block other calls.
25   * <p>
26   * Subsequent calls to deleteWhenPossible() don't block.
27   *
28   * <p>A call to deleteWhenPossible() always results in the file being (eventually) deleted.
29   *
30   * <p>Both stream() and deleteWhenPossible() can be called multiple times, in any order, with any
31   * level of concurrency.
32   *
33   * @since v2.13
34   */
35  public class CachedFile {
36      static enum State {UNCACHED, CACHED, NEEDSDELETE, DELETED}
37      interface FileOpener {
38          OutputStream open(File file) throws IOException;
39      }
40      static class FileInputStreamOpener implements FileOpener {
41          @Override
42          public OutputStream open(File file) throws IOException {
43              return new FileOutputStream(file);
44          }
45      }
46  
47      private static final Logger log = LoggerFactory.getLogger(CachedFile.class);
48      
49      private final File tmpFile;
50      private final FileOpener fileOpener;
51  
52      /** guards read/writes to `concurrentCount` and `state` */
53      private final Object lock = new Object();
54      private int concurrentCount;
55      private State state = State.UNCACHED;
56  
57      public CachedFile(File tmpFile) {
58          this(tmpFile, new FileInputStreamOpener());
59      }
60  
61      CachedFile(File tmpFile, final FileOpener fileOpener) {
62          this.tmpFile = tmpFile;
63          this.fileOpener = fileOpener;
64      }
65  
66      public void stream(OutputStream dest, FileCacheStreamProvider input) throws DownloadException {
67  
68          boolean fromCache = doEnter(input);
69          try {
70              if (fromCache) {
71                  streamFromCache(dest);
72              } else {
73                  input.writeStream(dest);
74              }
75          } finally {
76              doExit();
77          }
78      }
79  
80      State getState() {
81          return state;
82      }
83  
84      /**
85       * Increments the concurrent count, and returns if it is okay
86       * to read from the cached file, or not.
87       * If necessary, it will create the cached file first.
88       * doExit() must be called iff this method returns normally
89       */
90      private boolean doEnter(FileCacheStreamProvider input) {
91          synchronized (lock) {
92              boolean useCache = false;
93              if (state == State.UNCACHED) {
94                  try {
95                      streamToCache(input);
96                      state = State.CACHED;
97                      useCache = true;
98                  } catch (Exception e) {
99                      log.warn("Problem caching to disk, skipping cache for this entry", e);
100                     state = State.DELETED; // don't bother caching at all
101                 }
102             } else if (state == State.CACHED) {
103                 useCache = true;
104             } else if (state == State.NEEDSDELETE) {
105                 useCache = true; // need to delete, but allow concurrent callers to use it while it's there
106             }
107             
108             concurrentCount++;
109             return useCache && tmpFile.isFile(); // don't use the cache if the file disappeared
110         }
111     }
112 
113     /**
114      * Decrements the concurrent count. Will delete the cache if requested, and there are no
115      * other concurrent users.
116      */
117     private void doExit() {
118         synchronized (lock) {
119             concurrentCount--;
120             if (state == State.NEEDSDELETE && concurrentCount == 0) {
121                 tmpFile.delete();
122                 state = State.DELETED;
123             }
124         }
125     }
126 
127     public void deleteWhenPossible() {
128         synchronized (lock) {
129             if (state == State.UNCACHED) {
130                 state = State.DELETED;
131             } else if (state == State.CACHED) {
132                 state = State.NEEDSDELETE;
133             }
134 
135             if (state == State.NEEDSDELETE && concurrentCount == 0) {
136                 tmpFile.delete();
137                 state = State.DELETED;
138             }
139         }
140     }
141 
142     private void streamToCache(FileCacheStreamProvider input) throws IOException, DownloadException {
143         OutputStream cacheout = null;
144         try {
145             cacheout = new BufferedOutputStream(fileOpener.open(tmpFile));
146             input.writeStream(cacheout);
147         } finally {
148             if (cacheout != null) {
149                 cacheout.close(); // NB: if this fails, we don't want to silently ignore it
150             }
151         }
152     }
153 
154     private void streamFromCache(OutputStream out) throws DownloadException {
155         InputStream in = null;
156         try {
157             in = new FileInputStream(tmpFile);
158             IOUtils.copyLarge(in, out);
159             out.flush();
160         } catch (IOException e) {
161             throw new DownloadException(e);
162         } finally {
163             IOUtils.closeQuietly(in);
164         }
165     }
166 
167 }