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;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.apache.commons.vfs2.FileName;
029import org.apache.commons.vfs2.FileObject;
030import org.apache.commons.vfs2.FileSystem;
031
032/**
033 * This implementation caches every file as long as it is strongly reachable by the JVM. As soon as the JVM needs
034 * memory - every softly reachable file will be discarded.
035 *
036 * @see SoftReference
037 */
038public class SoftRefFilesCache extends AbstractFilesCache {
039
040    /**
041     * This thread will listen on the ReferenceQueue and remove the entry in the file cache as soon as the JVM removes
042     * the reference.
043     */
044    private final class ReleaseThread extends Thread {
045        private ReleaseThread() {
046            setName(ReleaseThread.class.getName());
047            setDaemon(true);
048        }
049
050        @Override
051        public void run() {
052            try {
053                while (true) {
054                    removeFile(refQueue.remove(0));
055                }
056            } catch (final InterruptedException e) {
057                // end thread run.
058                // System.out.println("Thread caught InterruptedException, ending " + getId());
059                // System.out.flush();
060            }
061        }
062    }
063
064    private static final Log log = LogFactory.getLog(SoftRefFilesCache.class);
065    private final Map<FileSystem, Map<FileName, Reference<FileObject>>> fileSystemCache = new HashMap<>();
066    private final Map<Reference<FileObject>, FileSystemAndNameKey> refReverseMap = new HashMap<>(100);
067    private final ReferenceQueue<FileObject> refQueue = new ReferenceQueue<>();
068    private ReleaseThread releaseThread;
069
070    /**
071     * Constructs a new instance.
072     */
073    public SoftRefFilesCache() {
074        // empty
075    }
076
077    @Override
078    public synchronized void clear(final FileSystem fileSystem) {
079        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileSystem);
080        final Iterator<FileSystemAndNameKey> iterKeys = refReverseMap.values().iterator();
081
082        while (iterKeys.hasNext()) {
083            final FileSystemAndNameKey key = iterKeys.next();
084            if (key.getFileSystem() == fileSystem) {
085                iterKeys.remove();
086                files.remove(key.getFileName());
087            }
088        }
089
090        if (files.isEmpty()) {
091            close(fileSystem);
092        }
093    }
094
095    @Override
096    public synchronized void close() {
097        super.close();
098        endThread();
099        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}