/* * Copyright 2012 Evernote Corporation * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.evernote.client.conn.mobile; import android.support.annotation.NonNull; import com.squareup.okhttp.internal.Util; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Holds all the data in memory until a threshold is reached. Then it writes all the data on disk. * * @author rwondratschek */ public class DiskBackedByteStore extends ByteStore { private static final int DEFAULT_MEMORY_BUFFER_SIZE = 2 * 1024 * 1024; /** * The maximum amount of memory to use before writing to disk. */ protected final int mMaxMemory; protected final File mCacheDir; protected final LazyByteArrayOutputStream mByteArrayOutputStream; protected File mCacheFile; protected OutputStream mCurrentOutputStream; protected FileOutputStream mFileOutputStream; protected int mBytesWritten; protected boolean mClosed; protected byte[] mData; protected byte[] mFileBuffer; /** * @param cacheDir A directory where the temporary data is stored. * @param maxMemory The threshold before the data is written to disk. */ protected DiskBackedByteStore(File cacheDir, int maxMemory) { mCacheDir = cacheDir; mMaxMemory = maxMemory; mByteArrayOutputStream = new LazyByteArrayOutputStream(); mCurrentOutputStream = mByteArrayOutputStream; } @Override public void write(@NonNull byte[] buffer, int offset, int count) throws IOException { initBuffers(); swapIfNecessary(count); mCurrentOutputStream.write(buffer, offset, count); mBytesWritten += count; } @Override public void write(int oneByte) throws IOException { initBuffers(); swapIfNecessary(1); mCurrentOutputStream.write(oneByte); mBytesWritten++; } private void initBuffers() throws IOException { if (mClosed) { throw new IOException("Already closed"); } if (mCurrentOutputStream == null) { if (swapped()) { mCurrentOutputStream = mFileOutputStream; } else { mCurrentOutputStream = mByteArrayOutputStream; } } } private void swapIfNecessary(int delta) throws IOException { if (isSwapRequired(delta)) { swapToDisk(); } } private boolean isSwapRequired(int delta) { return !swapped() && mBytesWritten + delta > mMaxMemory; } protected boolean swapped() { return mBytesWritten > mMaxMemory; } protected void swapToDisk() throws IOException { if (!mCacheDir.exists() && !mCacheDir.mkdirs()) { throw new IOException("could not create cache dir"); } if (!mCacheDir.isDirectory()) { throw new IOException("cache dir is no directory"); } mCacheFile = File.createTempFile("byte_store", null, mCacheDir); mFileOutputStream = new FileOutputStream(mCacheFile); mByteArrayOutputStream.writeTo(mFileOutputStream); mByteArrayOutputStream.reset(); mCurrentOutputStream = mFileOutputStream; } @Override public void close() throws IOException { if (!mClosed) { Util.closeQuietly(mFileOutputStream); mByteArrayOutputStream.reset(); mClosed = true; } } @Override public int getBytesWritten() { return mBytesWritten; } @Override public byte[] getData() throws IOException { if (mData != null) { return mData; } close(); if (swapped()) { if (mFileBuffer == null || mFileBuffer.length < mBytesWritten) { mFileBuffer = new byte[mBytesWritten]; } readFile(mCacheFile, mFileBuffer, mBytesWritten); mData = mFileBuffer; } else { mData = mByteArrayOutputStream.toByteArray(); } return mData; } @Override public void reset() throws IOException { try { close(); if (mCacheFile != null && mCacheFile.isFile()) { if (!mCacheFile.delete()) { throw new IOException("could not delete cache file"); } } } finally { mFileOutputStream = null; mCurrentOutputStream = null; mBytesWritten = 0; mClosed = false; mData = null; } } private static void readFile(File file, byte[] buffer, int length) throws IOException { InputStream inputStream = null; try { inputStream = new FileInputStream(file); int read = 0; int offset = 0; while (length > 0 && read >= 0) { read = inputStream.read(buffer, offset, length); offset += read; length -= read; } } finally { Util.closeQuietly(inputStream); } } public static class Factory implements ByteStore.Factory { private final File mCacheDir; private final int mMaxMemory; /** * @param cacheDir A directory where the temporary data is stored. */ public Factory(File cacheDir) { this(cacheDir, DEFAULT_MEMORY_BUFFER_SIZE); } /** * @param cacheDir A directory where the temporary data is stored. * @param maxMemory The threshold before the data is written to disk. */ public Factory(File cacheDir, int maxMemory) { mCacheDir = cacheDir; mMaxMemory = maxMemory; } @Override public DiskBackedByteStore create() { return new DiskBackedByteStore(mCacheDir, mMaxMemory); } } }