View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.cache;
18  
19  import java.lang.ref.Reference;
20  import java.lang.ref.ReferenceQueue;
21  import java.lang.ref.SoftReference;
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.Map;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.apache.commons.vfs2.FileName;
29  import org.apache.commons.vfs2.FileObject;
30  import org.apache.commons.vfs2.FileSystem;
31  
32  /**
33   * This implementation caches every file as long as it is strongly reachable by the JVM. As soon as the JVM needs
34   * memory - every softly reachable file will be discarded.
35   *
36   * @see SoftReference
37   */
38  public class SoftRefFilesCache extends AbstractFilesCache {
39  
40      /**
41       * This thread will listen on the ReferenceQueue and remove the entry in the file cache as soon as the JVM removes
42       * the reference.
43       */
44      private final class ReleaseThread extends Thread {
45          private ReleaseThread() {
46              setName(ReleaseThread.class.getName());
47              setDaemon(true);
48          }
49  
50          @Override
51          public void run() {
52              try {
53                  while (true) {
54                      removeFile(refQueue.remove(0));
55                  }
56              } catch (final InterruptedException e) {
57                  // end thread run.
58                  // System.out.println("Thread caught InterruptedException, ending " + getId());
59                  // System.out.flush();
60              }
61          }
62      }
63  
64      private static final Log log = LogFactory.getLog(SoftRefFilesCache.class);
65      private final Map<FileSystem, Map<FileName, Reference<FileObject>>> fileSystemCache = new HashMap<>();
66      private final Map<Reference<FileObject>, FileSystemAndNameKey> refReverseMap = new HashMap<>(100);
67      private final ReferenceQueue<FileObject> refQueue = new ReferenceQueue<>();
68      private ReleaseThread releaseThread;
69  
70      /**
71       * Constructs a new instance.
72       */
73      public SoftRefFilesCache() {
74          // empty
75      }
76  
77      @Override
78      public synchronized void clear(final FileSystem fileSystem) {
79          final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileSystem);
80          final Iterator<FileSystemAndNameKey> iterKeys = refReverseMap.values().iterator();
81  
82          while (iterKeys.hasNext()) {
83              final FileSystemAndNameKey key = iterKeys.next();
84              if (key.getFileSystem() == fileSystem) {
85                  iterKeys.remove();
86                  files.remove(key.getFileName());
87              }
88          }
89  
90          if (files.isEmpty()) {
91              close(fileSystem);
92          }
93      }
94  
95      @Override
96      public synchronized void close() {
97          super.close();
98          endThread();
99          fileSystemCache.clear();
100         refReverseMap.clear();
101     }
102 
103     /**
104      * @param fileSystem The file system to close.
105      */
106     private synchronized void close(final FileSystem fileSystem) {
107         if (log.isDebugEnabled()) {
108             log.debug("Close FileSystem: " + fileSystem.getRootName().getFriendlyURI());
109         }
110 
111         fileSystemCache.remove(fileSystem);
112         if (fileSystemCache.isEmpty()) {
113             endThread();
114         }
115     }
116 
117     /**
118      * Constructs a new Reference.
119      *
120      * @param file a file object.
121      * @param referenceQueue a ReferenceQueue.
122      * @return a new Reference on the given input.
123      */
124     protected Reference<FileObject> createReference(final FileObject file, final ReferenceQueue<FileObject> referenceQueue) {
125         return new SoftReference<>(file, referenceQueue);
126     }
127 
128     private synchronized void endThread() {
129         final ReleaseThread thread = releaseThread;
130         releaseThread = null;
131         if (thread != null) {
132             thread.interrupt();
133         }
134     }
135 
136     @Override
137     public synchronized FileObject getFile(final FileSystem fileSystem, final FileName fileName) {
138         final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileSystem);
139 
140         final Reference<FileObject> ref = files.get(fileName);
141         if (ref == null) {
142             return null;
143         }
144 
145         final FileObject fo = ref.get();
146         if (fo == null) {
147             removeFile(fileSystem, fileName);
148         }
149         return fo;
150     }
151 
152     /**
153      * Gets or creates a new Map.
154      *
155      * @param fileSystem the key.
156      * @return an existing or new Map.
157      */
158     protected synchronized Map<FileName, Reference<FileObject>> getOrCreateFilesystemCache(final FileSystem fileSystem) {
159         if (fileSystemCache.isEmpty()) {
160             startThread();
161         }
162         return fileSystemCache.computeIfAbsent(fileSystem, k -> new HashMap<>());
163     }
164 
165     private String getSafeName(final FileName fileName) {
166         return fileName.getFriendlyURI();
167     }
168 
169     private String getSafeName(final FileObject fileObject) {
170         return this.getSafeName(fileObject.getName());
171     }
172 
173     @Override
174     public void putFile(final FileObject fileObject) {
175         if (log.isDebugEnabled()) {
176             log.debug("putFile: " + this.getSafeName(fileObject));
177         }
178 
179         synchronized (this) {
180             final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileObject.getFileSystem());
181 
182             final Reference<FileObject> ref = createReference(fileObject, refQueue);
183             final FileSystemAndNameKey key = new FileSystemAndNameKey(fileObject.getFileSystem(), fileObject.getName());
184 
185             final Reference<FileObject> old = files.put(fileObject.getName(), ref);
186             if (old != null) {
187                 refReverseMap.remove(old);
188             }
189             refReverseMap.put(ref, key);
190         }
191     }
192 
193     @Override
194     public boolean putFileIfAbsent(final FileObject fileObject) {
195         if (log.isDebugEnabled()) {
196             log.debug("putFile: " + this.getSafeName(fileObject));
197         }
198 
199         synchronized (this) {
200             final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileObject.getFileSystem());
201 
202             final Reference<FileObject> ref = createReference(fileObject, refQueue);
203             final FileSystemAndNameKey key = new FileSystemAndNameKey(fileObject.getFileSystem(), fileObject.getName());
204 
205             final Reference<FileObject> reference = files.get(fileObject.getName());
206             if (reference != null && reference.get() != null) {
207                 return false;
208             }
209             final Reference<FileObject> old = files.put(fileObject.getName(), ref);
210             if (old != null) {
211                 refReverseMap.remove(old);
212             }
213             refReverseMap.put(ref, key);
214             return true;
215         }
216     }
217 
218     @Override
219     public synchronized void removeFile(final FileSystem fileSystem, final FileName fileName) {
220         if (removeFile(new FileSystemAndNameKey(fileSystem, fileName))) {
221             close(fileSystem);
222         }
223     }
224 
225     private synchronized boolean removeFile(final FileSystemAndNameKey key) {
226         if (log.isDebugEnabled()) {
227             log.debug("removeFile: " + this.getSafeName(key.getFileName()));
228         }
229 
230         final Map<?, ?> files = getOrCreateFilesystemCache(key.getFileSystem());
231 
232         final Object ref = files.remove(key.getFileName());
233         if (ref != null) {
234             refReverseMap.remove(ref);
235         }
236 
237         return files.isEmpty();
238     }
239 
240     private synchronized void removeFile(final Reference<?> ref) {
241         final FileSystemAndNameKey key = refReverseMap.get(ref);
242         if (key != null && removeFile(key)) {
243             close(key.getFileSystem());
244         }
245     }
246 
247     private synchronized void startThread() {
248         if (releaseThread == null) {
249             releaseThread = new ReleaseThread();
250             releaseThread.start();
251             // System.out.println("Started thread ID " + releaseThread.getId());
252             // System.out.flush();
253             // Thread.dumpStack();
254         }
255     }
256 
257     @Override
258     public String toString() {
259         return super.toString() + " [releaseThread=" + releaseThread
260             + (releaseThread == null ? "" : "(ID " + releaseThread.getId() + " is " + releaseThread.getState() + ")")
261             + "]";
262     }
263 }