From 71c6301eb49d37c4aef8d70b8e94b5403d5627cc Mon Sep 17 00:00:00 2001 From: Alexey Danilov Date: Fri, 5 Aug 2016 14:36:25 +0300 Subject: [PATCH] :metal: cache source's info in db to increase performance (#41, #45) --- .../java/com/danikula/videocache/Config.java | 5 +- .../danikula/videocache/HttpProxyCache.java | 2 +- .../videocache/HttpProxyCacheServer.java | 8 +- .../HttpProxyCacheServerClients.java | 2 +- .../danikula/videocache/HttpUrlSource.java | 66 +++++++------ .../danikula/videocache/Preconditions.java | 8 +- .../com/danikula/videocache/SourceInfo.java | 28 ++++++ .../DatabaseSourceInfoStorage.java | 98 +++++++++++++++++++ .../sourcestorage/NoSourceInfoStorage.java | 24 +++++ .../sourcestorage/SourceInfoStorage.java | 17 ++++ .../SourceInfoStorageFactory.java | 19 ++++ sample/build.gradle | 4 +- .../videocache/HttpProxyCacheTest.java | 61 ++++++++++++ .../videocache/HttpUrlSourceTest.java | 30 ++++++ .../danikula/videocache/ProxyCacheTest.java | 13 +-- .../sourcestorage/SourceInfoStorageTest.java | 89 +++++++++++++++++ .../support/AngryHttpUrlSource.java | 33 ------- .../support/PhlegmaticByteArraySource.java | 32 ------ .../support/ProxyCacheTestUtils.java | 52 ++++++++++ 19 files changed, 481 insertions(+), 110 deletions(-) create mode 100644 library/src/main/java/com/danikula/videocache/SourceInfo.java create mode 100644 library/src/main/java/com/danikula/videocache/sourcestorage/DatabaseSourceInfoStorage.java create mode 100644 library/src/main/java/com/danikula/videocache/sourcestorage/NoSourceInfoStorage.java create mode 100644 library/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorage.java create mode 100644 library/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorageFactory.java create mode 100644 test/src/test/java/com/danikula/videocache/sourcestorage/SourceInfoStorageTest.java delete mode 100644 test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java delete mode 100644 test/src/test/java/com/danikula/videocache/support/PhlegmaticByteArraySource.java diff --git a/library/src/main/java/com/danikula/videocache/Config.java b/library/src/main/java/com/danikula/videocache/Config.java index bcf4e77..b7ab899 100644 --- a/library/src/main/java/com/danikula/videocache/Config.java +++ b/library/src/main/java/com/danikula/videocache/Config.java @@ -2,6 +2,7 @@ package com.danikula.videocache; import com.danikula.videocache.file.DiskUsage; import com.danikula.videocache.file.FileNameGenerator; +import com.danikula.videocache.sourcestorage.SourceInfoStorage; import java.io.File; @@ -15,11 +16,13 @@ class Config { public final File cacheRoot; public final FileNameGenerator fileNameGenerator; public final DiskUsage diskUsage; + public final SourceInfoStorage sourceInfoStorage; - Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage) { + Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage) { this.cacheRoot = cacheRoot; this.fileNameGenerator = fileNameGenerator; this.diskUsage = diskUsage; + this.sourceInfoStorage = sourceInfoStorage; } File generateCacheFile(String url) { diff --git a/library/src/main/java/com/danikula/videocache/HttpProxyCache.java b/library/src/main/java/com/danikula/videocache/HttpProxyCache.java index 0620fde..3564832 100644 --- a/library/src/main/java/com/danikula/videocache/HttpProxyCache.java +++ b/library/src/main/java/com/danikula/videocache/HttpProxyCache.java @@ -101,7 +101,7 @@ class HttpProxyCache extends ProxyCache { @Override protected void onCachePercentsAvailableChanged(int percents) { if (listener != null) { - listener.onCacheAvailable(cache.file, source.url, percents); + listener.onCacheAvailable(cache.file, source.getUrl(), percents); } } } diff --git a/library/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java b/library/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java index 3a45881..b5f0282 100644 --- a/library/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java +++ b/library/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java @@ -9,6 +9,8 @@ import com.danikula.videocache.file.FileNameGenerator; import com.danikula.videocache.file.Md5FileNameGenerator; import com.danikula.videocache.file.TotalCountLruDiskUsage; import com.danikula.videocache.file.TotalSizeLruDiskUsage; +import com.danikula.videocache.sourcestorage.SourceInfoStorage; +import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory; import java.io.File; import java.io.IOException; @@ -192,6 +194,8 @@ public class HttpProxyCacheServer { shutdownClients(); + config.sourceInfoStorage.release(); + waitConnectionThread.interrupt(); try { if (!serverSocket.isClosed()) { @@ -364,8 +368,10 @@ public class HttpProxyCacheServer { private File cacheRoot; private FileNameGenerator fileNameGenerator; private DiskUsage diskUsage; + private SourceInfoStorage sourceInfoStorage; public Builder(Context context) { + this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context); this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context); this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE); this.fileNameGenerator = new Md5FileNameGenerator(); @@ -439,7 +445,7 @@ public class HttpProxyCacheServer { } private Config buildConfig() { - return new Config(cacheRoot, fileNameGenerator, diskUsage); + return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage); } } diff --git a/library/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java b/library/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java index 8296d2a..2ad3992 100644 --- a/library/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java +++ b/library/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java @@ -79,7 +79,7 @@ final class HttpProxyCacheServerClients { } private HttpProxyCache newHttpProxyCache() throws ProxyCacheException { - HttpUrlSource source = new HttpUrlSource(url); + HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage); FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage); HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache); httpProxyCache.registerCacheListener(uiCacheListener); diff --git a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java index 52da075..e64d653 100644 --- a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java +++ b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java @@ -3,6 +3,9 @@ package com.danikula.videocache; import android.text.TextUtils; import android.util.Log; +import com.danikula.videocache.sourcestorage.SourceInfoStorage; +import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory; + import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; @@ -10,6 +13,7 @@ import java.io.InterruptedIOException; import java.net.HttpURLConnection; import java.net.URL; +import static com.danikula.videocache.Preconditions.checkNotNull; import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE; import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG; import static java.net.HttpURLConnection.HTTP_MOVED_PERM; @@ -26,51 +30,53 @@ import static java.net.HttpURLConnection.HTTP_SEE_OTHER; public class HttpUrlSource implements Source { private static final int MAX_REDIRECTS = 5; - public final String url; + private final SourceInfoStorage sourceInfoStorage; + private SourceInfo sourceInfo; private HttpURLConnection connection; private InputStream inputStream; - private volatile int length = Integer.MIN_VALUE; - private volatile String mime; public HttpUrlSource(String url) { - this(url, ProxyCacheUtils.getSupposablyMime(url)); + this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage()); } - public HttpUrlSource(String url, String mime) { - this.url = Preconditions.checkNotNull(url); - this.mime = mime; + public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) { + this.sourceInfoStorage = checkNotNull(sourceInfoStorage); + SourceInfo sourceInfo = sourceInfoStorage.get(url); + this.sourceInfo = sourceInfo != null ? sourceInfo : + new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url)); } public HttpUrlSource(HttpUrlSource source) { - this.url = source.url; - this.mime = source.mime; - this.length = source.length; + this.sourceInfo = source.sourceInfo; + this.sourceInfoStorage = source.sourceInfoStorage; } @Override public synchronized int length() throws ProxyCacheException { - if (length == Integer.MIN_VALUE) { + if (sourceInfo.length == Integer.MIN_VALUE) { fetchContentInfo(); } - return length; + return sourceInfo.length; } @Override public void open(int offset) throws ProxyCacheException { try { connection = openConnection(offset, -1); - mime = connection.getContentType(); + String mime = connection.getContentType(); inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE); - length = readSourceAvailableBytes(connection, offset, connection.getResponseCode()); + int length = readSourceAvailableBytes(connection, offset, connection.getResponseCode()); + this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime); + this.sourceInfoStorage.put(sourceInfo.url, sourceInfo); } catch (IOException e) { - throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e); + throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e); } } private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException { int contentLength = connection.getContentLength(); return responseCode == HTTP_OK ? contentLength - : responseCode == HTTP_PARTIAL ? contentLength + offset : length; + : responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length; } @Override @@ -90,29 +96,31 @@ public class HttpUrlSource implements Source { @Override public int read(byte[] buffer) throws ProxyCacheException { if (inputStream == null) { - throw new ProxyCacheException("Error reading data from " + url + ": connection is absent!"); + throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!"); } try { return inputStream.read(buffer, 0, buffer.length); } catch (InterruptedIOException e) { - throw new InterruptedProxyCacheException("Reading source " + url + " is interrupted", e); + throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e); } catch (IOException e) { - throw new ProxyCacheException("Error reading data from " + url, e); + throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e); } } private void fetchContentInfo() throws ProxyCacheException { - Log.d(LOG_TAG, "Read content info from " + url); + Log.d(LOG_TAG, "Read content info from " + sourceInfo.url); HttpURLConnection urlConnection = null; InputStream inputStream = null; try { urlConnection = openConnection(0, 10000); - length = urlConnection.getContentLength(); - mime = urlConnection.getContentType(); + int length = urlConnection.getContentLength(); + String mime = urlConnection.getContentType(); inputStream = urlConnection.getInputStream(); - Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length); + this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime); + this.sourceInfoStorage.put(sourceInfo.url, sourceInfo); + Log.i(LOG_TAG, "Source info fetched: " + sourceInfo); } catch (IOException e) { - Log.e(LOG_TAG, "Error fetching info from " + url, e); + Log.e(LOG_TAG, "Error fetching info from " + sourceInfo.url, e); } finally { ProxyCacheUtils.close(inputStream); if (urlConnection != null) { @@ -125,7 +133,7 @@ public class HttpUrlSource implements Source { HttpURLConnection connection; boolean redirected; int redirectCount = 0; - String url = this.url; + String url = this.sourceInfo.url; do { Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url); connection = (HttpURLConnection) new URL(url).openConnection(); @@ -151,18 +159,18 @@ public class HttpUrlSource implements Source { } public synchronized String getMime() throws ProxyCacheException { - if (TextUtils.isEmpty(mime)) { + if (TextUtils.isEmpty(sourceInfo.mime)) { fetchContentInfo(); } - return mime; + return sourceInfo.mime; } public String getUrl() { - return url; + return sourceInfo.url; } @Override public String toString() { - return "HttpUrlSource{url='" + url + "}"; + return "HttpUrlSource{sourceInfo='" + sourceInfo + "}"; } } diff --git a/library/src/main/java/com/danikula/videocache/Preconditions.java b/library/src/main/java/com/danikula/videocache/Preconditions.java index bff4193..28e0390 100644 --- a/library/src/main/java/com/danikula/videocache/Preconditions.java +++ b/library/src/main/java/com/danikula/videocache/Preconditions.java @@ -1,15 +1,15 @@ package com.danikula.videocache; -final class Preconditions { +public final class Preconditions { - static T checkNotNull(T reference) { + public static T checkNotNull(T reference) { if (reference == null) { throw new NullPointerException(); } return reference; } - static void checkAllNotNull(Object... references) { + public static void checkAllNotNull(Object... references) { for (Object reference : references) { if (reference == null) { throw new NullPointerException(); @@ -17,7 +17,7 @@ final class Preconditions { } } - static T checkNotNull(T reference, String errorMessage) { + public static T checkNotNull(T reference, String errorMessage) { if (reference == null) { throw new NullPointerException(errorMessage); } diff --git a/library/src/main/java/com/danikula/videocache/SourceInfo.java b/library/src/main/java/com/danikula/videocache/SourceInfo.java new file mode 100644 index 0000000..0756afc --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/SourceInfo.java @@ -0,0 +1,28 @@ +package com.danikula.videocache; + +/** + * Stores source's info. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public class SourceInfo { + + public final String url; + public final int length; + public final String mime; + + public SourceInfo(String url, int length, String mime) { + this.url = url; + this.length = length; + this.mime = mime; + } + + @Override + public String toString() { + return "SourceInfo{" + + "url='" + url + '\'' + + ", length=" + length + + ", mime='" + mime + '\'' + + '}'; + } +} diff --git a/library/src/main/java/com/danikula/videocache/sourcestorage/DatabaseSourceInfoStorage.java b/library/src/main/java/com/danikula/videocache/sourcestorage/DatabaseSourceInfoStorage.java new file mode 100644 index 0000000..04ba9c4 --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/sourcestorage/DatabaseSourceInfoStorage.java @@ -0,0 +1,98 @@ +package com.danikula.videocache.sourcestorage; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import com.danikula.videocache.SourceInfo; + +import static com.danikula.videocache.Preconditions.checkAllNotNull; +import static com.danikula.videocache.Preconditions.checkNotNull; + +/** + * Database based {@link SourceInfoStorage}. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage { + + private static final String TABLE = "SourceInfo"; + private static final String COLUMN_ID = "_id"; + private static final String COLUMN_URL = "url"; + private static final String COLUMN_LENGTH = "length"; + private static final String COLUMN_MIME = "mime"; + private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME}; + private static final String CREATE_SQL = + "CREATE TABLE " + TABLE + " (" + + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + COLUMN_URL + " TEXT NOT NULL," + + COLUMN_MIME + " TEXT," + + COLUMN_LENGTH + " INTEGER" + + ");"; + + DatabaseSourceInfoStorage(Context context) { + super(context, "AndroidVideoCache.db", null, 1); + checkNotNull(context); + } + + @Override + public void onCreate(SQLiteDatabase db) { + checkNotNull(db); + db.execSQL(CREATE_SQL); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + throw new IllegalStateException("Should not be called. There is no any migration"); + } + + @Override + public SourceInfo get(String url) { + checkNotNull(url); + Cursor cursor = null; + try { + cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null); + return cursor == null || !cursor.moveToFirst() ? null : convert(cursor); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @Override + public void put(String url, SourceInfo sourceInfo) { + checkAllNotNull(url, sourceInfo); + SourceInfo sourceInfoFromDb = get(url); + boolean exist = sourceInfoFromDb != null; + ContentValues contentValues = convert(sourceInfo); + if (exist) { + getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url}); + } else { + getWritableDatabase().insert(TABLE, null, contentValues); + } + } + + @Override + public void release() { + close(); + } + + private SourceInfo convert(Cursor cursor) { + return new SourceInfo( + cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)), + cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)), + cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME)) + ); + } + + private ContentValues convert(SourceInfo sourceInfo) { + ContentValues values = new ContentValues(); + values.put(COLUMN_URL, sourceInfo.url); + values.put(COLUMN_LENGTH, sourceInfo.length); + values.put(COLUMN_MIME, sourceInfo.mime); + return values; + } +} diff --git a/library/src/main/java/com/danikula/videocache/sourcestorage/NoSourceInfoStorage.java b/library/src/main/java/com/danikula/videocache/sourcestorage/NoSourceInfoStorage.java new file mode 100644 index 0000000..e2c08c4 --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/sourcestorage/NoSourceInfoStorage.java @@ -0,0 +1,24 @@ +package com.danikula.videocache.sourcestorage; + +import com.danikula.videocache.SourceInfo; + +/** + * {@link SourceInfoStorage} that does nothing. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public class NoSourceInfoStorage implements SourceInfoStorage { + + @Override + public SourceInfo get(String url) { + return null; + } + + @Override + public void put(String url, SourceInfo sourceInfo) { + } + + @Override + public void release() { + } +} diff --git a/library/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorage.java b/library/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorage.java new file mode 100644 index 0000000..5e97d6d --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorage.java @@ -0,0 +1,17 @@ +package com.danikula.videocache.sourcestorage; + +import com.danikula.videocache.SourceInfo; + +/** + * Storage for {@link SourceInfo}. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public interface SourceInfoStorage { + + SourceInfo get(String url); + + void put(String url, SourceInfo sourceInfo); + + void release(); +} diff --git a/library/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorageFactory.java b/library/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorageFactory.java new file mode 100644 index 0000000..6861f73 --- /dev/null +++ b/library/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorageFactory.java @@ -0,0 +1,19 @@ +package com.danikula.videocache.sourcestorage; + +import android.content.Context; + +/** + * Simple factory for {@link SourceInfoStorage}. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +public class SourceInfoStorageFactory { + + public static SourceInfoStorage newSourceInfoStorage(Context context) { + return new DatabaseSourceInfoStorage(context); + } + + public static SourceInfoStorage newEmptySourceInfoStorage() { + return new NoSourceInfoStorage(); + } +} diff --git a/sample/build.gradle b/sample/build.gradle index f0e5939..2eed779 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -35,10 +35,10 @@ apt { } dependencies { -// compile project(':library') + compile project(':library') compile 'com.android.support:support-v4:23.1.0' compile 'org.androidannotations:androidannotations-api:3.3.2' - compile 'com.danikula:videocache:2.5.0' +// compile 'com.danikula:videocache:2.5.0' compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar' apt 'org.androidannotations:androidannotations:3.3.2' } diff --git a/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java b/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java index a02b4e2..e28e259 100644 --- a/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java +++ b/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java @@ -1,6 +1,9 @@ package com.danikula.videocache; +import com.danikula.android.garden.io.IoUtils; import com.danikula.videocache.file.FileCache; +import com.danikula.videocache.sourcestorage.SourceInfoStorage; +import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory; import com.danikula.videocache.support.ProxyCacheTestUtils; import com.danikula.videocache.support.Response; import com.danikula.videocache.test.BuildConfig; @@ -9,6 +12,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.io.ByteArrayOutputStream; @@ -23,12 +27,15 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME; +import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME; import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL; import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_SIZE; import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL; import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; import static com.danikula.videocache.support.ProxyCacheTestUtils.loadTestData; +import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile; import static org.fest.assertions.api.Assertions.assertThat; +import static org.fest.assertions.api.Assertions.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; @@ -159,6 +166,60 @@ public class HttpProxyCacheTest { assertThat(response.data).isEmpty(); } + @Test(expected = ProxyCacheException.class) + public void testTouchSourceForAbsentSourceInfoAndCache() throws Exception { + SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage(); + HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage); + HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(newCacheFile())); + processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1"); + proxyCache.shutdown(); + fail("Angry source should throw error! There is no file and caches source info"); + } + + @Test(expected = ProxyCacheException.class) + public void testTouchSourceForExistedSourceInfoAndAbsentCache() throws Exception { + SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application); + sourceInfoStorage.put(HTTP_DATA_URL, new SourceInfo(HTTP_DATA_URL, HTTP_DATA_SIZE, "image/jpg")); + HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage); + HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(newCacheFile())); + processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1"); + proxyCache.shutdown(); + fail("Angry source should throw error! There is no cache file"); + } + + @Test + public void testTouchSourceForExistedSourceInfoAndCache() throws Exception { + SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application); + sourceInfoStorage.put(HTTP_DATA_URL, new SourceInfo(HTTP_DATA_URL, HTTP_DATA_SIZE, "cached/mime")); + HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage); + File file = newCacheFile(); + IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), file); + HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(file)); + Response response = processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1"); + proxyCache.shutdown(); + assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME)); + assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE); + assertThat(response.contentType).isEqualTo("cached/mime"); + } + + @Test + public void testReuseSourceInfo() throws Exception { + SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application); + HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL, sourceInfoStorage); + File cacheFile = newCacheFile(); + HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(cacheFile)); + processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1"); + + HttpUrlSource notOpenableSource = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage); + HttpProxyCache proxyCache2 = new HttpProxyCache(notOpenableSource, new FileCache(cacheFile)); + Response response = processRequest(proxyCache2, "GET /" + HTTP_DATA_URL + " HTTP/1.1"); + proxyCache.shutdown(); + + assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME)); + assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE); + assertThat(response.contentType).isEqualTo("image/jpeg"); + } + private Response processRequest(String sourceUrl, String httpRequest) throws ProxyCacheException, IOException { FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile()); return processRequest(sourceUrl, httpRequest, fileCache); diff --git a/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java b/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java index 13b4707..504b302 100644 --- a/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java +++ b/test/src/test/java/com/danikula/videocache/HttpUrlSourceTest.java @@ -1,10 +1,14 @@ package com.danikula.videocache; +import com.danikula.videocache.sourcestorage.SourceInfoStorage; +import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory; +import com.danikula.videocache.support.ProxyCacheTestUtils; import com.danikula.videocache.test.BuildConfig; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.annotation.Config; @@ -24,6 +28,7 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL_ import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; import static org.fest.assertions.api.Assertions.assertThat; import static org.fest.assertions.api.Assertions.fail; +import static org.mockito.Matchers.any; /** * @author Alexey Danilov (danikula@gmail.com). @@ -132,6 +137,31 @@ public class HttpUrlSourceTest { assertThat(new HttpUrlSource(HTTP_DATA_URL).getMime()).isEqualTo("image/jpeg"); } + @Test(expected = RuntimeException.class) + public void testAngryHttpUrlSourceLength() throws Exception { + ProxyCacheTestUtils.newAngryHttpUrlSource().length(); + fail("source.length() should throw exception"); + } + + @Test(expected = RuntimeException.class) + public void testAngryHttpUrlSourceOpen() throws Exception { + ProxyCacheTestUtils.newAngryHttpUrlSource().open(Mockito.anyInt()); + fail("source.open() should throw exception"); + } + + @Test(expected = RuntimeException.class) + public void testAngryHttpUrlSourceRead() throws Exception { + ProxyCacheTestUtils.newAngryHttpUrlSource().read(any(byte[].class)); + fail("source.read() should throw exception"); + } + + @Test(expected = RuntimeException.class) + public void testNotOpenableHttpUrlSourceOpen() throws Exception { + SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage(); + ProxyCacheTestUtils.newNotOpenableHttpUrlSource("", sourceInfoStorage).open(Mockito.anyInt()); + fail("source.open() should throw exception"); + } + private void readSource(Source source, byte[] target) throws ProxyCacheException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int totalRead = 0; diff --git a/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java b/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java index d52658f..e4fee0d 100644 --- a/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java +++ b/test/src/test/java/com/danikula/videocache/ProxyCacheTest.java @@ -2,8 +2,7 @@ package com.danikula.videocache; import com.danikula.android.garden.io.IoUtils; import com.danikula.videocache.file.FileCache; -import com.danikula.videocache.support.AngryHttpUrlSource; -import com.danikula.videocache.support.PhlegmaticByteArraySource; +import com.danikula.videocache.support.ProxyCacheTestUtils; import com.danikula.videocache.test.BuildConfig; import org.junit.Test; @@ -23,6 +22,7 @@ import static com.danikula.videocache.support.ProxyCacheTestUtils.generate; import static com.danikula.videocache.support.ProxyCacheTestUtils.getFileContent; import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile; +import static com.danikula.videocache.support.ProxyCacheTestUtils.newPhlegmaticSource; import static org.fest.assertions.api.Assertions.assertThat; /** @@ -95,7 +95,7 @@ public class ProxyCacheTest { public void testProxyWithPhlegmaticSource() throws Exception { int dataSize = 100000; byte[] sourceData = generate(dataSize); - Source source = new PhlegmaticByteArraySource(sourceData, 200); + Source source = newPhlegmaticSource(sourceData, 200); ProxyCache proxyCache = new ProxyCache(source, new FileCache(newCacheFile())); byte[] readData = new byte[dataSize]; proxyCache.read(readData, 0, dataSize); @@ -106,7 +106,7 @@ public class ProxyCacheTest { @Test public void testReadEnd() throws Exception { int capacity = 5323; - Source source = new PhlegmaticByteArraySource(generate(capacity), 200); + Source source = newPhlegmaticSource(generate(capacity), 200); Cache cache = new FileCache(newCacheFile()); ProxyCache proxyCache = new ProxyCache(source, cache); proxyCache.read(new byte[1], capacity - 1, 1); @@ -118,7 +118,7 @@ public class ProxyCacheTest { public void testReadRandomParts() throws Exception { int dataSize = 123456; byte[] sourceData = generate(dataSize); - Source source = new PhlegmaticByteArraySource(sourceData, 300); + Source source = newPhlegmaticSource(sourceData, 300); File file = newCacheFile(); Cache cache = new FileCache(file); ProxyCache proxyCache = new ProxyCache(source, cache); @@ -176,8 +176,9 @@ public class ProxyCacheTest { byte[] data = generate(dataSize); File file = newCacheFile(); IoUtils.saveToFile(data, file); - ProxyCache proxyCache = new ProxyCache(new AngryHttpUrlSource(), new FileCache(file)); + Source source = ProxyCacheTestUtils.newAngryHttpUrlSource(); + ProxyCache proxyCache = new ProxyCache(source, new FileCache(file)); byte[] readData = new byte[dataSize]; proxyCache.read(readData, 0, dataSize); diff --git a/test/src/test/java/com/danikula/videocache/sourcestorage/SourceInfoStorageTest.java b/test/src/test/java/com/danikula/videocache/sourcestorage/SourceInfoStorageTest.java new file mode 100644 index 0000000..5d0ae56 --- /dev/null +++ b/test/src/test/java/com/danikula/videocache/sourcestorage/SourceInfoStorageTest.java @@ -0,0 +1,89 @@ +package com.danikula.videocache.sourcestorage; + +import com.danikula.videocache.SourceInfo; +import com.danikula.videocache.test.BuildConfig; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.fest.assertions.api.Assertions.fail; + +/** + * Tests for {@link SourceInfoStorage}. + * + * @author Alexey Danilov (danikula@gmail.com). + */ +@RunWith(RobolectricGradleTestRunner.class) +@Config(constants = BuildConfig.class) +public class SourceInfoStorageTest { + + private SourceInfoStorage storage; + + @Before + public void setUp() throws Exception { + storage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application); + } + + @After + public void tearDown() throws Exception { + storage.release(); + } + + @Test + public void testGetAbsent() throws Exception { + SourceInfo sourceInfo = storage.get(":-)"); + assertThat(sourceInfo).isNull(); + } + + @Test + public void testSaving() throws Exception { + storage.put(":-)", new SourceInfo(":-)", 42, "text/plain")); + storage.put(":-(", new SourceInfo(":-(", 43, "video/mp4")); + + SourceInfo sourceInfo = storage.get(":-)"); + assertThat(sourceInfo.url).isEqualTo(":-)"); + assertThat(sourceInfo.length).isEqualTo(42); + assertThat(sourceInfo.mime).isEqualTo("text/plain"); + + SourceInfo sourceInfo2 = storage.get(":-("); + assertThat(sourceInfo2.url).isEqualTo(":-("); + assertThat(sourceInfo2.length).isEqualTo(43); + assertThat(sourceInfo2.mime).isEqualTo("video/mp4"); + } + + @Test + public void testUpdating() throws Exception { + String url = ":-)"; + storage.put(url, new SourceInfo(url, 42, "text/plain")); + storage.put(url, new SourceInfo(url, 43, "video/mp4")); + + SourceInfo sourceInfo = storage.get(url); + assertThat(sourceInfo.url).isEqualTo(url); + assertThat(sourceInfo.length).isEqualTo(43); + assertThat(sourceInfo.mime).isEqualTo("video/mp4"); + } + + @Test(expected = NullPointerException.class) + public void testNpeForGetting() throws Exception { + storage.get(null); + fail("null is not acceptable"); + } + + @Test(expected = NullPointerException.class) + public void testNpeForPuttingUrl() throws Exception { + storage.put(null, new SourceInfo("", 0, "")); + fail("null is not acceptable"); + } + + @Test(expected = NullPointerException.class) + public void testNpeForPuttingSource() throws Exception { + storage.put("url", null); + fail("null is not acceptable"); + } +} diff --git a/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java b/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java deleted file mode 100644 index 220837f..0000000 --- a/test/src/test/java/com/danikula/videocache/support/AngryHttpUrlSource.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.danikula.videocache.support; - -import com.danikula.videocache.ProxyCacheException; -import com.danikula.videocache.Source; - -/** - * {@link Source} that throws exception in all methods. - * - * @author Alexey Danilov (danikula@gmail.com). - */ -@Deprecated // use Mockito to throw error -public class AngryHttpUrlSource implements Source { - - @Override - public int length() throws ProxyCacheException { - throw new IllegalStateException(); - } - - @Override - public void open(int offset) throws ProxyCacheException { - throw new IllegalStateException(); - } - - @Override - public void close() throws ProxyCacheException { - throw new IllegalStateException(); - } - - @Override - public int read(byte[] buffer) throws ProxyCacheException { - throw new IllegalStateException(); - } -} diff --git a/test/src/test/java/com/danikula/videocache/support/PhlegmaticByteArraySource.java b/test/src/test/java/com/danikula/videocache/support/PhlegmaticByteArraySource.java deleted file mode 100644 index 387c7c7..0000000 --- a/test/src/test/java/com/danikula/videocache/support/PhlegmaticByteArraySource.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.danikula.videocache.support; - -import com.danikula.videocache.ByteArraySource; -import com.danikula.videocache.ProxyCacheException; - -import java.util.Random; - -/** - * @author Alexey Danilov (danikula@gmail.com). - */ -@Deprecated // TODO: use Mockito to mock delay -public class PhlegmaticByteArraySource extends ByteArraySource { - - private final Random delayGenerator; - private final int maxDelayMs; - - public PhlegmaticByteArraySource(byte[] data, int maxDelayMs) { - super(data); - this.maxDelayMs = maxDelayMs; - this.delayGenerator = new Random(System.currentTimeMillis()); - } - - @Override - public int read(byte[] buffer) throws ProxyCacheException { - try { - Thread.sleep(delayGenerator.nextInt(maxDelayMs)); - } catch (InterruptedException e) { - throw new ProxyCacheException("Error sleeping", e); - } - return super.read(buffer); - } -} diff --git a/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java b/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java index 400d020..4a5e754 100644 --- a/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java +++ b/test/src/test/java/com/danikula/videocache/support/ProxyCacheTestUtils.java @@ -1,9 +1,16 @@ package com.danikula.videocache.support; import com.danikula.android.garden.io.IoUtils; +import com.danikula.videocache.ByteArraySource; import com.danikula.videocache.HttpProxyCacheServer; +import com.danikula.videocache.HttpUrlSource; +import com.danikula.videocache.ProxyCacheException; +import com.danikula.videocache.Source; +import com.danikula.videocache.sourcestorage.SourceInfoStorage; import com.google.common.io.Files; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.robolectric.RuntimeEnvironment; import java.io.ByteArrayOutputStream; @@ -15,6 +22,13 @@ import java.net.URL; import java.util.Random; import java.util.UUID; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + /** * @author Alexey Danilov (danikula@gmail.com). */ @@ -83,4 +97,42 @@ public class ProxyCacheTestUtils { random.nextBytes(result); return result; } + + public static HttpUrlSource newAngryHttpUrlSource() throws ProxyCacheException { + HttpUrlSource source = mock(HttpUrlSource.class); + doThrow(new RuntimeException()).when(source).getMime(); + doThrow(new RuntimeException()).when(source).read(any(byte[].class)); + doThrow(new RuntimeException()).when(source).open(anyInt()); + doThrow(new RuntimeException()).when(source).length(); + doThrow(new RuntimeException()).when(source).getUrl(); + doThrow(new RuntimeException()).when(source).close(); + return source; + } + + public static HttpUrlSource newNotOpenableHttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) throws ProxyCacheException { + HttpUrlSource httpUrlSource = new HttpUrlSource(url, sourceInfoStorage); + HttpUrlSource source = spy(httpUrlSource); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + System.out.print("Can't open!!!"); + throw new RuntimeException(); + } + }).when(source).open(anyInt()); + return source; + } + + public static Source newPhlegmaticSource(byte[] data, final int maxDelayMs) throws ProxyCacheException { + Source spySource = spy(new ByteArraySource(data)); + final Random delayGenerator = new Random(System.currentTimeMillis()); + doAnswer(new Answer() { + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Thread.sleep(delayGenerator.nextInt(maxDelayMs)); + return null; + } + }).doCallRealMethod().when(spySource).read(any(byte[].class)); + return spySource; + } }