001/*
002 * (C) Copyright 2011-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Stephane Lacoin
016 *     Florent Guillaume
017 */
018package org.nuxeo.ecm.core.blob.binary;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.io.OutputStreamWriter;
027import java.io.Writer;
028import java.util.Map;
029
030import org.apache.commons.io.FileUtils;
031import org.apache.commons.io.IOUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.common.file.FileCache;
035import org.nuxeo.common.file.LRUFileCache;
036import org.nuxeo.common.utils.SizeUtils;
037import org.nuxeo.ecm.core.api.NuxeoException;
038import org.nuxeo.runtime.api.Framework;
039import org.nuxeo.runtime.trackers.files.FileEventTracker;
040
041/**
042 * Abstract class for a {@link BinaryManager} that uses a cache for its files because fetching them is expensive.
043 * <p>
044 * Initialization of the {@link BinaryManager} must call {@link #initializeCache} from the {@link #initialize} method.
045 *
046 * @since 5.7
047 */
048public abstract class CachingBinaryManager extends AbstractBinaryManager {
049
050    private static final Log log = LogFactory.getLog(CachingBinaryManager.class);
051
052    protected static final String LEN_DIGEST_SUFFIX = "-len";
053
054    protected File cachedir;
055
056    public FileCache fileCache;
057
058    protected FileStorage fileStorage;
059
060    @Override
061    public void initialize(String blobProviderId, Map<String, String> properties) throws IOException {
062        super.initialize(blobProviderId, properties);
063        descriptor = new BinaryManagerRootDescriptor();
064        descriptor.digest = getDefaultDigestAlgorithm();
065        log.info("Registering binary manager '" + blobProviderId + "' using " + getClass().getSimpleName());
066    }
067
068    /**
069     * Initialize the cache.
070     *
071     * @param dir the directory to use to store cached files
072     * @param maxSize the maximum size of the cache (in bytes)
073     * @param fileStorage the file storage mechanism to use to store and fetch files
074     * @since 5.9.2
075     */
076    public void initializeCache(File dir, long maxSize, @SuppressWarnings("hiding") FileStorage fileStorage) {
077        fileCache = new LRUFileCache(dir, maxSize);
078        this.fileStorage = fileStorage;
079    }
080
081    /**
082     * Initialize the cache.
083     *
084     * @param cacheSizeStr the maximum size of the cache (as a String)
085     * @param fileStorage the file storage mechanism to use to store and fetch files
086     * @since 6.0
087     * @see #initializeCache(File, long, FileStorage)
088     * @see SizeUtils#parseSizeInBytes(String)
089     */
090    public void initializeCache(String cacheSizeStr, @SuppressWarnings("hiding") FileStorage fileStorage)
091            throws IOException {
092        cachedir = File.createTempFile("nxbincache.", "", null);
093        cachedir.delete();
094        cachedir.mkdir();
095        long cacheSize = SizeUtils.parseSizeInBytes(cacheSizeStr);
096        initializeCache(cachedir, cacheSize, fileStorage);
097        log.info("Using binary cache directory: " + cachedir.getPath() + " size: " + cacheSizeStr);
098
099        // be sure FileTracker won't steal our files !
100        FileEventTracker.registerProtectedPath(cachedir.getAbsolutePath());
101    }
102
103    @Override
104    public void close() {
105        fileCache.clear();
106        if (cachedir != null) {
107            try {
108                FileUtils.deleteDirectory(cachedir);
109            } catch (IOException e) {
110                throw new NuxeoException(e);
111            }
112        }
113    }
114
115    @Override
116    protected Binary getBinary(InputStream in) throws IOException {
117        // write the input stream to a temporary file, while computing a digest
118        File tmp = fileCache.getTempFile();
119        OutputStream out = new FileOutputStream(tmp);
120        String digest;
121        try {
122            digest = storeAndDigest(in, out);
123        } finally {
124            in.close();
125            out.close();
126        }
127
128        File cachedFile = fileCache.getFile(digest);
129        if (cachedFile != null) {
130            // file already in cache
131            if (Framework.isTestModeSet()) {
132                Framework.getProperties().setProperty("cachedBinary", digest);
133            }
134            // delete tmp file, not needed anymore
135            tmp.delete();
136            return new Binary(cachedFile, digest, blobProviderId);
137        }
138
139        // send the file to storage
140        fileStorage.storeFile(digest, tmp);
141
142        // register the file in the file cache if all went well
143        File file = fileCache.putFile(digest, tmp);
144
145        return new Binary(file, digest, blobProviderId);
146    }
147
148    @Override
149    public Binary getBinary(String digest) {
150        // Check in the cache
151        File file = fileCache.getFile(digest);
152        if (file == null) {
153            return new LazyBinary(digest, blobProviderId, this);
154        } else {
155            return new Binary(file, digest, blobProviderId);
156        }
157    }
158
159    /* =============== Methods used by LazyBinary =============== */
160
161    /**
162     * Gets a file from cache or storage.
163     * <p>
164     * Used by {@link LazyBinary}.
165     */
166    public File getFile(String digest) throws IOException {
167        // get file from cache
168        File file = fileCache.getFile(digest);
169        if (file != null) {
170            return file;
171        }
172        // fetch file from storage
173        File tmp = fileCache.getTempFile();
174        if (fileStorage.fetchFile(digest, tmp)) {
175            // put file in cache
176            file = fileCache.putFile(digest, tmp);
177            return file;
178        } else {
179            // file not in storage
180            tmp.delete();
181            return null;
182        }
183    }
184
185    /**
186     * Gets a file length from cache or storage.
187     * <p>
188     * Use by {@link LazyBinary}.
189     */
190    public Long getLength(String digest) throws IOException {
191        // get length from cache
192        Long length = getLengthFromCache(digest);
193        if (length != null) {
194            return length;
195        }
196        // fetch length from storage
197        length = fileStorage.fetchLength(digest);
198        // put length in cache
199        putLengthInCache(digest, length);
200        return length;
201    }
202
203    protected Long getLengthFromCache(String digest) throws IOException {
204        File f = fileCache.getFile(digest + LEN_DIGEST_SUFFIX);
205        if (f == null) {
206            return null;
207        }
208        // read decimal length from file
209        InputStream in = null;
210        try {
211            in = new FileInputStream(f);
212            String len = IOUtils.toString(in);
213            return Long.valueOf(len);
214        } catch (NumberFormatException e) {
215            throw new IOException("Invalid length in " + f, e);
216        } finally {
217            IOUtils.closeQuietly(in);
218        }
219    }
220
221    protected void putLengthInCache(String digest, Long len) throws IOException {
222        // write decimal length in file
223        OutputStream out = null;
224        try {
225            File tmp = fileCache.getTempFile();
226            out = new FileOutputStream(tmp);
227            Writer writer = new OutputStreamWriter(out);
228            writer.write(len.toString());
229            writer.flush();
230            writer.close();
231            fileCache.putFile(digest + LEN_DIGEST_SUFFIX, tmp);
232        } finally {
233            IOUtils.closeQuietly(out);
234        }
235    }
236
237}