Change the Storage class to be immutable
Also make sure the in-memory state and the database are modified together while a lock is being held.
This commit is contained in:
parent
ec04a1c827
commit
ffa0ebb5bb
9 changed files with 142 additions and 189 deletions
|
@ -32,6 +32,7 @@ class Preferences internal constructor(
|
|||
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : AccountManager {
|
||||
private val accountLock = Any()
|
||||
private val storageLock = Any()
|
||||
|
||||
@GuardedBy("accountLock")
|
||||
private var accountsMap: MutableMap<String, Account>? = null
|
||||
|
@ -44,15 +45,22 @@ class Preferences internal constructor(
|
|||
private val accountsChangeListeners = CopyOnWriteArraySet<AccountsChangeListener>()
|
||||
private val accountRemovedListeners = CopyOnWriteArraySet<AccountRemovedListener>()
|
||||
|
||||
val storage = Storage()
|
||||
@GuardedBy("storageLock")
|
||||
private var currentStorage: Storage? = null
|
||||
|
||||
init {
|
||||
val persistedStorageValues = storagePersister.loadValues()
|
||||
storage.replaceAll(persistedStorageValues)
|
||||
}
|
||||
val storage: Storage
|
||||
get() = synchronized(storageLock) {
|
||||
currentStorage ?: storagePersister.loadValues().also { newStorage ->
|
||||
currentStorage = newStorage
|
||||
}
|
||||
}
|
||||
|
||||
fun createStorageEditor(): StorageEditor {
|
||||
return storagePersister.createStorageEditor(storage)
|
||||
return storagePersister.createStorageEditor { updater ->
|
||||
synchronized(storageLock) {
|
||||
currentStorage = updater(storage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.TESTS)
|
||||
|
|
|
@ -2,30 +2,31 @@ package com.fsck.k9.preferences;
|
|||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Storage {
|
||||
private volatile Map<String, String> storage = Collections.emptyMap();
|
||||
private final Map<String, String> values;
|
||||
|
||||
public Storage() { }
|
||||
public Storage(Map<String, String> values) {
|
||||
this.values = Collections.unmodifiableMap(values);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return storage.isEmpty();
|
||||
return values.isEmpty();
|
||||
}
|
||||
|
||||
public boolean contains(String key) {
|
||||
return storage.containsKey(key);
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
public Map<String, String> getAll() {
|
||||
return storage;
|
||||
return values;
|
||||
}
|
||||
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
String val = storage.get(key);
|
||||
String val = values.get(key);
|
||||
if (val == null) {
|
||||
return defValue;
|
||||
}
|
||||
|
@ -33,7 +34,7 @@ public class Storage {
|
|||
}
|
||||
|
||||
public int getInt(String key, int defValue) {
|
||||
String val = storage.get(key);
|
||||
String val = values.get(key);
|
||||
if (val == null) {
|
||||
return defValue;
|
||||
}
|
||||
|
@ -46,7 +47,7 @@ public class Storage {
|
|||
}
|
||||
|
||||
public long getLong(String key, long defValue) {
|
||||
String val = storage.get(key);
|
||||
String val = values.get(key);
|
||||
if (val == null) {
|
||||
return defValue;
|
||||
}
|
||||
|
@ -59,14 +60,10 @@ public class Storage {
|
|||
}
|
||||
|
||||
public String getString(String key, String defValue) {
|
||||
String val = storage.get(key);
|
||||
String val = values.get(key);
|
||||
if (val == null) {
|
||||
return defValue;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
public void replaceAll(Map<String, String> workingStorage) {
|
||||
storage = new HashMap<>(workingStorage);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package com.fsck.k9.preferences
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
|
||||
interface StoragePersister {
|
||||
@CheckResult
|
||||
fun loadValues(): Map<String, String>
|
||||
fun loadValues(): Storage
|
||||
|
||||
fun createStorageEditor(storage: Storage): StorageEditor
|
||||
fun createStorageEditor(storageUpdater: StorageUpdater): StorageEditor
|
||||
}
|
||||
|
||||
fun interface StorageUpdater {
|
||||
fun updateStorage(updater: (currentStorage: Storage) -> Storage)
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
package com.fsck.k9.preferences
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
private const val TEST_STRING_KEY = "s"
|
||||
private const val TEST_STRING_VALUE = "y"
|
||||
private const val TEST_INT_KEY = "i"
|
||||
private const val TEST_INT_VALUE = "4"
|
||||
private const val TEST_STRING_DEFAULT = "z"
|
||||
private const val TEST_INT_DEFAULT = 2
|
||||
private val TEST_MAP = mapOf(
|
||||
TEST_STRING_KEY to TEST_STRING_VALUE,
|
||||
TEST_INT_KEY to TEST_INT_VALUE
|
||||
)
|
||||
|
||||
class StorageTest {
|
||||
internal var storage = Storage()
|
||||
|
||||
@Test
|
||||
fun isEmpty() {
|
||||
assertTrue(storage.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isNotEmpty() {
|
||||
storage.replaceAll(TEST_MAP)
|
||||
assertFalse(storage.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun contains() {
|
||||
storage.replaceAll(TEST_MAP)
|
||||
assertTrue(storage.contains(TEST_STRING_KEY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getString() {
|
||||
storage.replaceAll(TEST_MAP)
|
||||
assertEquals(TEST_STRING_VALUE, storage.getString(TEST_STRING_KEY, TEST_STRING_DEFAULT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getString_default() {
|
||||
assertTrue(storage.isEmpty)
|
||||
assertEquals(TEST_STRING_DEFAULT, storage.getString(TEST_STRING_KEY, TEST_STRING_DEFAULT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAll() {
|
||||
storage.replaceAll(TEST_MAP)
|
||||
assertFalse(storage.isEmpty)
|
||||
assertEquals(TEST_MAP, storage.all)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replaceAll() {
|
||||
storage.replaceAll(TEST_MAP)
|
||||
storage.replaceAll(emptyMap())
|
||||
assertTrue(storage.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getInteger() {
|
||||
storage.replaceAll(TEST_MAP)
|
||||
|
||||
assertEquals(Integer.parseInt(TEST_INT_VALUE), storage.getInt(TEST_INT_KEY, TEST_INT_DEFAULT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getInteger_stringValue() {
|
||||
storage.replaceAll(TEST_MAP)
|
||||
|
||||
// TODO is this good behavior?
|
||||
assertEquals(TEST_INT_DEFAULT, storage.getInt(TEST_STRING_KEY, TEST_INT_DEFAULT))
|
||||
}
|
||||
}
|
|
@ -15,18 +15,16 @@ import timber.log.Timber;
|
|||
|
||||
|
||||
public class K9StorageEditor implements StorageEditor {
|
||||
private Storage storage;
|
||||
private StorageUpdater storageUpdater;
|
||||
private K9StoragePersister storagePersister;
|
||||
|
||||
private Map<String, String> changes = new HashMap<>();
|
||||
private List<String> removals = new ArrayList<>();
|
||||
private Map<String, String> snapshot = new HashMap<>();
|
||||
|
||||
|
||||
public K9StorageEditor(Storage storage, K9StoragePersister storagePersister) {
|
||||
this.storage = storage;
|
||||
public K9StorageEditor(StorageUpdater storageUpdater, K9StoragePersister storagePersister) {
|
||||
this.storageUpdater = storageUpdater;
|
||||
this.storagePersister = storagePersister;
|
||||
snapshot.putAll(storage.getAll());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -47,7 +45,7 @@ public class K9StorageEditor implements StorageEditor {
|
|||
@Override
|
||||
public boolean commit() {
|
||||
try {
|
||||
commitChanges();
|
||||
storageUpdater.updateStorage(this::commitChanges);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "Failed to save preferences");
|
||||
|
@ -55,13 +53,16 @@ public class K9StorageEditor implements StorageEditor {
|
|||
}
|
||||
}
|
||||
|
||||
private void commitChanges() {
|
||||
private Storage commitChanges(Storage storage) {
|
||||
long startTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Committing preference changes");
|
||||
|
||||
Map<String, String> newValues = new HashMap<>();
|
||||
Map<String, String> oldValues = storage.getAll();
|
||||
StoragePersistOperationCallback committer = new StoragePersistOperationCallback() {
|
||||
@Override
|
||||
public void beforePersistTransaction(Map<String, String> workingStorage) {
|
||||
workingStorage.putAll(storage.getAll());
|
||||
workingStorage.putAll(oldValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -72,7 +73,7 @@ public class K9StorageEditor implements StorageEditor {
|
|||
for (Entry<String, String> entry : changes.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String newValue = entry.getValue();
|
||||
String oldValue = snapshot.get(key);
|
||||
String oldValue = oldValues.get(key);
|
||||
if (removals.contains(key) || !newValue.equals(oldValue)) {
|
||||
ops.put(key, newValue);
|
||||
}
|
||||
|
@ -81,12 +82,14 @@ public class K9StorageEditor implements StorageEditor {
|
|||
|
||||
@Override
|
||||
public void onPersistTransactionSuccess(Map<String, String> workingStorage) {
|
||||
storage.replaceAll(workingStorage);
|
||||
newValues.putAll(workingStorage);
|
||||
}
|
||||
};
|
||||
storagePersister.doInTransaction(committer);
|
||||
long endTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Preferences commit took %d ms", endTime - startTime);
|
||||
|
||||
return new Storage(newValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -10,8 +10,8 @@ import android.database.Cursor;
|
|||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.CheckResult;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.preferences.migrations.StorageMigrations;
|
||||
import com.fsck.k9.preferences.migrations.StorageMigrationsHelper;
|
||||
|
@ -88,9 +88,10 @@ public class K9StoragePersister implements StoragePersister {
|
|||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public StorageEditor createStorageEditor(Storage storage) {
|
||||
return new K9StorageEditor(storage, this);
|
||||
public StorageEditor createStorageEditor(@NonNull StorageUpdater storageUpdater) {
|
||||
return new K9StorageEditor(storageUpdater, this);
|
||||
}
|
||||
|
||||
static class StoragePersistOperations {
|
||||
|
@ -136,14 +137,14 @@ public class K9StoragePersister implements StoragePersister {
|
|||
void onPersistTransactionSuccess(Map<String, String> workingStorage);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@CheckResult
|
||||
public Map<String, String> loadValues() {
|
||||
public Storage loadValues() {
|
||||
long startTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Loading preferences from DB into Storage");
|
||||
|
||||
try (SQLiteDatabase database = openDB()) {
|
||||
return readAllValues(database);
|
||||
return new Storage(readAllValues(database));
|
||||
} finally {
|
||||
long endTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Preferences load took %d ms", endTime - startTime);
|
||||
|
|
|
@ -1,47 +1,39 @@
|
|||
package com.fsck.k9.preferences
|
||||
|
||||
import com.fsck.k9.preferences.K9StoragePersister.StoragePersistOperationCallback
|
||||
import com.fsck.k9.preferences.K9StoragePersister.StoragePersistOperations
|
||||
import com.fsck.k9.storage.K9RobolectricTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.doThrow
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.stubbing
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.verifyNoMoreInteractions
|
||||
import org.mockito.kotlin.whenever
|
||||
|
||||
class StorageEditorTest : K9RobolectricTest() {
|
||||
@Mock private lateinit var storage: Storage
|
||||
@Mock private lateinit var storagePersister: K9StoragePersister
|
||||
@Mock private lateinit var storagePersisterOps: K9StoragePersister.StoragePersistOperations
|
||||
private lateinit var editor: K9StorageEditor
|
||||
private val storage: Storage = Storage(mapOf("storage-key" to "storage-value"))
|
||||
private val storageUpdater = TestStorageUpdater(storage)
|
||||
private val storagePersister = mock<K9StoragePersister>()
|
||||
private val storagePersisterOps = mock<StoragePersistOperations>()
|
||||
private val editor = K9StorageEditor(storageUpdater, storagePersister)
|
||||
|
||||
private val workingMap = mutableMapOf<String, String>()
|
||||
private val storageMap = mapOf(
|
||||
"storage-key" to "storage-value"
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
whenever(storage.all).thenReturn(storageMap)
|
||||
|
||||
editor = K9StorageEditor(storage, storagePersister)
|
||||
verify(storage).all
|
||||
}
|
||||
private val newValues: Map<String, String>
|
||||
get() = storageUpdater.newStorage!!.all
|
||||
|
||||
@Test
|
||||
fun commit_exception() {
|
||||
whenever(storagePersister.doInTransaction(any())).thenThrow(RuntimeException())
|
||||
stubbing(storagePersister) {
|
||||
on { doInTransaction(any()) } doThrow RuntimeException()
|
||||
}
|
||||
|
||||
val success = editor.commit()
|
||||
|
||||
assertFalse(success)
|
||||
assertThat(success).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -50,7 +42,9 @@ class StorageEditorTest : K9RobolectricTest() {
|
|||
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(storage.all)
|
||||
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
||||
|
@ -61,7 +55,8 @@ class StorageEditorTest : K9RobolectricTest() {
|
|||
editor.putBoolean("x", true)
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(mapOf("storage-key" to "storage-value", "x" to "true"))
|
||||
verify(storagePersisterOps).put("x", "true")
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
@ -73,7 +68,8 @@ class StorageEditorTest : K9RobolectricTest() {
|
|||
editor.putInt("x", 123)
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(mapOf("storage-key" to "storage-value", "x" to "123"))
|
||||
verify(storagePersisterOps).put("x", "123")
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
@ -85,7 +81,8 @@ class StorageEditorTest : K9RobolectricTest() {
|
|||
editor.putLong("x", 1234)
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(mapOf("storage-key" to "storage-value", "x" to "1234"))
|
||||
verify(storagePersisterOps).put("x", "1234")
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
@ -97,7 +94,8 @@ class StorageEditorTest : K9RobolectricTest() {
|
|||
editor.putString("x", "y")
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(mapOf("storage-key" to "storage-value", "x" to "y"))
|
||||
verify(storagePersisterOps).put("x", "y")
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
@ -109,7 +107,8 @@ class StorageEditorTest : K9RobolectricTest() {
|
|||
editor.putString("storage-key", "storage-value")
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(mapOf("storage-key" to "storage-value"))
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
||||
|
@ -120,7 +119,8 @@ class StorageEditorTest : K9RobolectricTest() {
|
|||
editor.putString("storage-key", "other-value")
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(mapOf("storage-key" to "other-value"))
|
||||
verify(storagePersisterOps).put("storage-key", "other-value")
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
@ -133,51 +133,71 @@ class StorageEditorTest : K9RobolectricTest() {
|
|||
editor.putString("storage-key", "storage-value")
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(mapOf("storage-key" to "storage-value"))
|
||||
verify(storagePersisterOps).remove("storage-key")
|
||||
verify(storagePersisterOps).put("storage-key", "storage-value")
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remove key that doesn't exist`() {
|
||||
prepareStoragePersisterMock()
|
||||
|
||||
editor.remove("x")
|
||||
val success = editor.commit()
|
||||
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEqualTo(mapOf("storage-key" to "storage-value"))
|
||||
verify(storagePersisterOps).remove("x")
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun remove() {
|
||||
prepareStoragePersisterMock()
|
||||
|
||||
editor.remove("x")
|
||||
editor.remove("storage-key")
|
||||
val success = editor.commit()
|
||||
|
||||
assertTrue(success)
|
||||
verify(storagePersisterOps).remove("x")
|
||||
assertThat(success).isTrue()
|
||||
assertThat(newValues).isEmpty()
|
||||
verify(storagePersisterOps).remove("storage-key")
|
||||
verifyNoMoreInteractions(storagePersisterOps)
|
||||
}
|
||||
|
||||
private fun prepareStoragePersisterMock() {
|
||||
whenever(storagePersisterOps.put(any(), any())).then {
|
||||
val key = it.getArgument<String>(0)
|
||||
val value = it.getArgument<String>(0)
|
||||
stubbing(storagePersisterOps) {
|
||||
on { put(any(), any()) } doAnswer {
|
||||
val key = it.getArgument<String>(0)
|
||||
val value = it.getArgument<String>(1)
|
||||
workingMap[key] = value
|
||||
}
|
||||
|
||||
workingMap[key] = value
|
||||
Unit
|
||||
}
|
||||
whenever(storagePersisterOps.remove(any())).then {
|
||||
val key = it.getArgument<String>(0)
|
||||
val value = it.getArgument<String>(0)
|
||||
on { remove(any()) } doAnswer {
|
||||
val key = it.getArgument<String>(0)
|
||||
workingMap.remove(key)
|
||||
|
||||
workingMap[key] = value
|
||||
Unit
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
whenever(storagePersister.doInTransaction(any())).then {
|
||||
val operationCallback = it.getArgument<K9StoragePersister.StoragePersistOperationCallback>(0)
|
||||
operationCallback.beforePersistTransaction(workingMap)
|
||||
verify(storage, times(2)).all
|
||||
assertEquals(workingMap, storageMap)
|
||||
stubbing(storagePersister) {
|
||||
on { doInTransaction(any()) } doAnswer {
|
||||
val operationCallback = it.getArgument<StoragePersistOperationCallback>(0)
|
||||
|
||||
operationCallback.persist(storagePersisterOps)
|
||||
verify(storagePersister).doInTransaction(any())
|
||||
|
||||
operationCallback.onPersistTransactionSuccess(workingMap)
|
||||
verify(storage).replaceAll(eq(workingMap))
|
||||
operationCallback.beforePersistTransaction(workingMap)
|
||||
operationCallback.persist(storagePersisterOps)
|
||||
operationCallback.onPersistTransactionSuccess(workingMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestStorageUpdater(private val currentStorage: Storage) : StorageUpdater {
|
||||
var newStorage: Storage? = null
|
||||
|
||||
override fun updateStorage(updater: (currentStorage: Storage) -> Storage) {
|
||||
newStorage = updater(currentStorage)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ class StoragePersisterTest : K9RobolectricTest() {
|
|||
|
||||
storagePersister.doInTransaction(operationCallback)
|
||||
|
||||
val values = storagePersister.loadValues()
|
||||
val values = storagePersister.loadValues().all
|
||||
assertEquals(1, values.size)
|
||||
assertEquals("y", values["x"])
|
||||
}
|
||||
|
|
|
@ -5,14 +5,15 @@ import android.content.SharedPreferences
|
|||
class InMemoryStoragePersister : StoragePersister {
|
||||
private val values = mutableMapOf<String, Any?>()
|
||||
|
||||
override fun loadValues(): Map<String, String> {
|
||||
return values.mapValues { (_, value) -> value?.toString() ?: "" }
|
||||
override fun loadValues(): Storage {
|
||||
return Storage(values.mapValues { (_, value) -> value?.toString() ?: "" })
|
||||
}
|
||||
|
||||
override fun createStorageEditor(storage: Storage): StorageEditor = InMemoryStorageEditor(storage)
|
||||
override fun createStorageEditor(storageUpdater: StorageUpdater): StorageEditor {
|
||||
return InMemoryStorageEditor(storageUpdater)
|
||||
}
|
||||
|
||||
private inner class InMemoryStorageEditor(private val storage: Storage) : StorageEditor {
|
||||
private val snapshot = storage.all.toMutableMap()
|
||||
private inner class InMemoryStorageEditor(private val storageUpdater: StorageUpdater) : StorageEditor {
|
||||
private val removals = mutableSetOf<String>()
|
||||
private val changes = mutableMapOf<String, String>()
|
||||
private var alreadyCommitted = false
|
||||
|
@ -52,12 +53,13 @@ class InMemoryStoragePersister : StoragePersister {
|
|||
if (alreadyCommitted) throw AssertionError("StorageEditor.commit() called more than once")
|
||||
alreadyCommitted = true
|
||||
|
||||
removals.forEach { snapshot.remove(it) }
|
||||
snapshot.putAll(changes)
|
||||
|
||||
storage.replaceAll(snapshot)
|
||||
storageUpdater.updateStorage(::writeValues)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun writeValues(currentStorage: Storage): Storage {
|
||||
return Storage(currentStorage.all - removals + changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue