001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.cache;
018
019import java.lang.ref.Reference;
020import java.lang.ref.ReferenceQueue;
021import java.lang.ref.SoftReference;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.Map;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.concurrent.ConcurrentMap;
027import java.util.concurrent.locks.Lock;
028import java.util.concurrent.locks.ReentrantLock;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.apache.commons.vfs2.FileName;
033import org.apache.commons.vfs2.FileObject;
034import org.apache.commons.vfs2.FileSystem;
035import org.apache.commons.vfs2.VfsLog;
036import org.apache.commons.vfs2.util.Messages;
037
038/**
039 * This implementation caches every file as long as it is strongly reachable by the java vm. As soon as the vm needs
040 * memory - every softly reachable file will be discarded.
041 *
042 * @see SoftReference
043 */
044public class SoftRefFilesCache extends AbstractFilesCache {
045    private static final int TIMEOUT = 1000;
046
047    private static final Log log = LogFactory.getLog(SoftRefFilesCache.class);
048
049    private final ConcurrentMap<FileSystem, Map<FileName, Reference<FileObject>>> fileSystemCache = new ConcurrentHashMap<>();
050    private final Map<Reference<FileObject>, FileSystemAndNameKey> refReverseMap = new HashMap<>(100);
051    private final ReferenceQueue<FileObject> refQueue = new ReferenceQueue<>();
052
053    private volatile SoftRefReleaseThread softRefReleaseThread = null; // @GuardedBy("lock")
054
055    private final Lock lock = new ReentrantLock();
056
057    /**
058     * This thread will listen on the ReferenceQueue and remove the entry in the filescache as soon as the vm removes
059     * the reference
060     */
061    private final class SoftRefReleaseThread extends Thread {
062        private volatile boolean requestEnd; // used for inter-thread communication
063
064        private SoftRefReleaseThread() {
065            setName(SoftRefReleaseThread.class.getName());
066            setDaemon(true);
067        }
068
069        @Override
070        public void run() {
071            loop: while (!requestEnd && !Thread.currentThread().isInterrupted()) {
072                try {
073                    final Reference<?> ref = refQueue.remove(TIMEOUT);
074                    if (ref == null) {
075                        continue;
076                    }
077
078                    lock.lock();
079                    try {
080                        final FileSystemAndNameKey key = refReverseMap.get(ref);
081
082                        if (key != null && removeFile(key)) {
083                            close(key.getFileSystem());
084                        }
085                    } finally {
086                        lock.unlock();
087                    }
088                } catch (final InterruptedException e) {
089                    if (!requestEnd) {
090                        VfsLog.warn(getLogger(), log,
091                                Messages.getString("vfs.impl/SoftRefReleaseThread-interrupt.info"));
092                    }
093                    break loop;
094                }
095            }
096        }
097    }
098
099    public SoftRefFilesCache() {
100    }
101
102    private void startThread() {
103        // Double Checked Locking is allowed when volatile
104        if (softRefReleaseThread != null) {
105            return;
106        }
107
108        synchronized (lock) {
109            if (softRefReleaseThread == null) {
110                softRefReleaseThread = new SoftRefReleaseThread();
111                softRefReleaseThread.start();
112            }
113        }
114    }
115
116    private void endThread() {
117        synchronized (lock) {
118            final SoftRefReleaseThread thread = softRefReleaseThread;
119            softRefReleaseThread = null;
120            if (thread != null) {
121                thread.requestEnd = true;
122                thread.interrupt();
123            }
124        }
125    }
126
127    @Override
128    public void putFile(final FileObject fileObject) {
129        if (log.isDebugEnabled()) {
130            log.debug("putFile: " + this.getSafeName(fileObject));
131        }
132
133        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileObject.getFileSystem());
134
135        final Reference<FileObject> ref = createReference(fileObject, refQueue);
136        final FileSystemAndNameKey key = new FileSystemAndNameKey(fileObject.getFileSystem(), fileObject.getName());
137
138        lock.lock();
139        try {
140            final Reference<FileObject> old = files.put(fileObject.getName(), ref);
141            if (old != null) {
142                refReverseMap.remove(old);
143            }
144            refReverseMap.put(ref, key);
145        } finally {
146            lock.unlock();
147        }
148    }
149
150    private String getSafeName(final FileName fileName) {
151        return fileName.getFriendlyURI();
152    }
153
154    private String getSafeName(final FileObject fileObject) {
155        return this.getSafeName(fileObject.getName());
156    }
157
158    @Override
159    public boolean putFileIfAbsent(final FileObject fileObject) {
160        if (log.isDebugEnabled()) {
161            log.debug("putFile: " + this.getSafeName(fileObject));
162        }
163
164        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileObject.getFileSystem());
165
166        final Reference<FileObject> ref = createReference(fileObject, refQueue);
167        final FileSystemAndNameKey key = new FileSystemAndNameKey(fileObject.getFileSystem(), fileObject.getName());
168
169        lock.lock();
170        try {
171            if (files.containsKey(fileObject.getName()) && files.get(fileObject.getName()).get() != null) {
172                return false;
173            }
174            final Reference<FileObject> old = files.put(fileObject.getName(), ref);
175            if (old != null) {
176                refReverseMap.remove(old);
177            }
178            refReverseMap.put(ref, key);
179            return true;
180        } finally {
181            lock.unlock();
182        }
183    }
184
185    protected Reference<FileObject> createReference(final FileObject file, final ReferenceQueue<FileObject> refqueue) {
186        return new SoftReference<>(file, refqueue);
187    }
188
189    @Override
190    public FileObject getFile(final FileSystem fileSystem, final FileName fileName) {
191        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileSystem);
192
193        lock.lock();
194        try {
195            final Reference<FileObject> ref = files.get(fileName);
196            if (ref == null) {
197                return null;
198            }
199
200            final FileObject fo = ref.get();
201            if (fo == null) {
202                removeFile(fileSystem, fileName);
203            }
204            return fo;
205        } finally {
206            lock.unlock();
207        }
208    }
209
210    @Override
211    public void clear(final FileSystem fileSystem) {
212        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileSystem);
213
214        lock.lock();
215        try {
216            final Iterator<FileSystemAndNameKey> iterKeys = refReverseMap.values().iterator();
217            while (iterKeys.hasNext()) {
218                final FileSystemAndNameKey key = iterKeys.next();
219                if (key.getFileSystem() == fileSystem) {
220                    iterKeys.remove();
221                    files.remove(key.getFileName());
222                }
223            }
224
225            if (files.size() < 1) {
226                close(fileSystem);
227            }
228        } finally {
229            lock.unlock();
230        }
231    }
232
233    /**
234     * Called while the lock is held
235     *
236     * @param fileSystem The file system to close.
237     */
238    private void close(final FileSystem fileSystem) {
239        if (log.isDebugEnabled()) {
240            log.debug("close fs: " + fileSystem.getRootName());
241        }
242
243        fileSystemCache.remove(fileSystem);
244        if (fileSystemCache.size() < 1) {
245            endThread();
246        }
247        /*
248         * This is not thread-safe as another thread might be opening the file system ((DefaultFileSystemManager)
249         * getContext().getFileSystemManager()) ._closeFileSystem(filesystem);
250         */
251    }
252
253    @Override
254    public void close() {
255        super.close();
256
257        endThread();
258
259        lock.lock();
260        try {
261            fileSystemCache.clear();
262
263            refReverseMap.clear();
264        } finally {
265            lock.unlock();
266        }
267    }
268
269    @Override
270    public void removeFile(final FileSystem fileSystem, final FileName fileName) {
271        if (removeFile(new FileSystemAndNameKey(fileSystem, fileName))) {
272            close(fileSystem);
273        }
274    }
275
276    private boolean removeFile(final FileSystemAndNameKey key) {
277        if (log.isDebugEnabled()) {
278            log.debug("removeFile: " + this.getSafeName(key.getFileName()));
279        }
280
281        final Map<?, ?> files = getOrCreateFilesystemCache(key.getFileSystem());
282
283        lock.lock();
284        try {
285            final Object ref = files.remove(key.getFileName());
286            if (ref != null) {
287                refReverseMap.remove(ref);
288            }
289
290            return files.size() < 1;
291        } finally {
292            lock.unlock();
293        }
294    }
295
296    protected Map<FileName, Reference<FileObject>> getOrCreateFilesystemCache(final FileSystem fileSystem) {
297        if (fileSystemCache.size() < 1) {
298            startThread();
299        }
300
301        Map<FileName, Reference<FileObject>> files;
302
303        do {
304            files = fileSystemCache.get(fileSystem);
305            if (files != null) {
306                break;
307            }
308            files = new HashMap<>();
309        } while (fileSystemCache.putIfAbsent(fileSystem, files) == null);
310
311        return files;
312    }
313}