Commit 7809908e authored by Sebastian Kaspari's avatar Sebastian Kaspari
Browse files

Issue #2216: Move code for saving and loading snapshots to AtomicFile extension methods.

parent c2551be9
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.session.ext
import android.util.AtomicFile
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.storage.SnapshotSerializer
import mozilla.components.concept.engine.Engine
import org.json.JSONException
import java.io.FileOutputStream
import java.io.IOException
/**
* Read a [SessionManager.Snapshot] from this [AtomicFile]. Returns `null` if no snapshot could be read.
*/
fun AtomicFile.readSnapshot(
engine: Engine,
serializer: SnapshotSerializer = SnapshotSerializer()
): SessionManager.Snapshot? {
return try {
openRead().use {
val json = it.bufferedReader().use { reader -> reader.readText() }
val snapshot = serializer.fromJSON(engine, json)
if (snapshot.isEmpty()) {
null
} else {
snapshot
}
}
} catch (_: IOException) {
null
} catch (_: JSONException) {
null
}
}
/**
* Saves the given [SessionManager.Snapshot] to this [AtomicFile].
*/
fun AtomicFile.writeSnapshot(
snapshot: SessionManager.Snapshot,
serializer: SnapshotSerializer = SnapshotSerializer()
): Boolean {
var outputStream: FileOutputStream? = null
return try {
val json = serializer.toJSON(snapshot)
outputStream = startWrite()
outputStream.write(json.toByteArray())
finishWrite(outputStream)
true
} catch (_: IOException) {
failWrite(outputStream)
false
} catch (_: JSONException) {
failWrite(outputStream)
false
}
}
......@@ -10,11 +10,10 @@ import android.support.annotation.VisibleForTesting
import android.support.annotation.WorkerThread
import android.util.AtomicFile
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.ext.readSnapshot
import mozilla.components.browser.session.ext.writeSnapshot
import mozilla.components.concept.engine.Engine
import org.json.JSONException
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.TimeUnit
private const val STORE_FILE_NAME_FORMAT = "mozilla_components_session_storage_%s.json"
......@@ -33,13 +32,18 @@ class SessionStorage(
/**
* Reads the saved state from disk. Returns null if no state was found on disk or if reading the file failed.
*/
@WorkerThread
fun restore(): SessionManager.Snapshot? {
return readSnapshotFromDisk(getFileForEngine(context, engine), serializer, engine)
synchronized(sessionFileLock) {
return getFileForEngine(context, engine)
.readSnapshot(engine, serializer)
}
}
/**
* Clears the state saved on disk.
*/
@WorkerThread
fun clear() {
removeSnapshotFromDisk(context, engine)
}
......@@ -54,7 +58,14 @@ class SessionStorage(
return true
}
return saveSnapshotToDisk(getFileForEngine(context, engine), serializer, snapshot)
requireNotNull(snapshot.sessions.getOrNull(snapshot.selectedSessionIndex)) {
"SessionSnapshot's selected index must be in bounds"
}
synchronized(sessionFileLock) {
return getFileForEngine(context, engine)
.writeSnapshot(snapshot, serializer)
}
}
/**
......@@ -70,71 +81,10 @@ class SessionStorage(
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@Suppress("ReturnCount")
internal fun readSnapshotFromDisk(
file: AtomicFile,
serializer: SnapshotSerializer,
engine: Engine
): SessionManager.Snapshot? {
synchronized(sessionFileLock) {
try {
file.openRead().use {
val json = it.bufferedReader().use { reader -> reader.readText() }
val snapshot = serializer.fromJSON(engine, json)
if (snapshot.isEmpty()) {
return null
}
return snapshot
}
} catch (_: IOException) {
return null
} catch (_: JSONException) {
return null
}
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun saveSnapshotToDisk(
file: AtomicFile,
serializer: SnapshotSerializer,
snapshot: SessionManager.Snapshot
): Boolean {
require(snapshot.sessions.isNotEmpty()) {
"SessionsSnapshot must not be empty"
}
requireNotNull(snapshot.sessions.getOrNull(snapshot.selectedSessionIndex)) {
"SessionSnapshot's selected index must be in bounds"
}
synchronized(sessionFileLock) {
var outputStream: FileOutputStream? = null
return try {
val json = serializer.toJSON(snapshot)
outputStream = file.startWrite()
outputStream.write(json.toByteArray())
file.finishWrite(outputStream)
true
} catch (_: IOException) {
file.failWrite(outputStream)
false
} catch (_: JSONException) {
file.failWrite(outputStream)
false
}
}
}
private fun removeSnapshotFromDisk(context: Context, engine: Engine) {
synchronized(sessionFileLock) {
getFileForEngine(context, engine).delete()
getFileForEngine(context, engine)
.delete()
}
}
......
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.session.ext
import android.util.AtomicFile
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.storage.SnapshotSerializer
import mozilla.components.browser.session.storage.getFileForEngine
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import org.junit.Assert.assertNull
import org.junit.Test
import org.mockito.Mockito
import org.robolectric.RuntimeEnvironment
import java.io.FileNotFoundException
import java.io.IOException
class AtomicFileKtTest {
@Test
fun `writeSnapshot - Fails write on IOException`() {
val file: AtomicFile = mock()
Mockito.doThrow(IOException::class.java).`when`(file).startWrite()
val snapshot = SessionManager.Snapshot(
sessions = listOf(
SessionManager.Snapshot.Item(Session("http://mozilla.org"))
),
selectedSessionIndex = 0
)
file.writeSnapshot(snapshot, SnapshotSerializer())
Mockito.verify(file).failWrite(any())
}
@Test
fun `readSnapshot - Returns null on FileNotFoundException`() {
val file: AtomicFile = mock()
Mockito.doThrow(FileNotFoundException::class.java).`when`(file).openRead()
val snapshot = file.readSnapshot(engine = mock(), serializer = SnapshotSerializer())
assertNull(snapshot)
}
@Test
fun `readSnapshot - Returns null on corrupt JSON`() {
val file = getFileForEngine(RuntimeEnvironment.application, engine = mock())
val stream = file.startWrite()
stream.bufferedWriter().write("{ name: 'Foo")
file.finishWrite(stream)
val snapshot = file.readSnapshot(engine = mock(), serializer = SnapshotSerializer())
assertNull(snapshot)
}
}
\ No newline at end of file
......@@ -7,7 +7,6 @@ package mozilla.components.browser.session.storage
import android.arch.lifecycle.Lifecycle
import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.LifecycleRegistry
import android.util.AtomicFile
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.session.Session
......@@ -30,7 +29,6 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.`when`
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doThrow
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
......@@ -38,8 +36,6 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import java.io.FileNotFoundException
import java.io.IOException
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
......@@ -441,44 +437,6 @@ class SessionStorageTest {
assertNotSame(completed, saveJob)
}
@Test
fun `saveSnapshotToDisk - Fails write on IOException`() {
val file: AtomicFile = mock()
doThrow(IOException::class.java).`when`(file).startWrite()
val snapshot = SessionManager.Snapshot(
sessions = listOf(
SessionManager.Snapshot.Item(Session("http://mozilla.org"))
),
selectedSessionIndex = 0
)
saveSnapshotToDisk(file, SnapshotSerializer(), snapshot)
verify(file).failWrite(any())
}
@Test
fun `readSnapshotFromDisk - Returns null on FileNotFoundException`() {
val file: AtomicFile = mock()
doThrow(FileNotFoundException::class.java).`when`(file).openRead()
val snapshot = readSnapshotFromDisk(file, engine = mock(), serializer = SnapshotSerializer())
assertNull(snapshot)
}
@Test
fun `readSnapshotFromDisk - Returns null on corrupt JSON`() {
val file = getFileForEngine(RuntimeEnvironment.application, engine = mock())
val stream = file.startWrite()
stream.bufferedWriter().write("{ name: 'Foo")
file.finishWrite(stream)
val snapshot = readSnapshotFromDisk(file, engine = mock(), serializer = SnapshotSerializer())
assertNull(snapshot)
}
@Test
fun deserializeWithInvalidSource() {
val json = JSONObject()
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment