diff --git a/README.md b/README.md index 4ebcf4f..2f4e3f2 100644 --- a/README.md +++ b/README.md @@ -22,39 +22,39 @@ Save data object. Your custom classes must have no-arg constructor. Paper creates separate data file for each key. ```java -Paper.put("city", "Lund"); // Primitive -Paper.put("task-queue", queue); // LinkedList -Paper.put("countries", countryCodeMap); // HashMap +Paper.book().write("city", "Lund"); // Primitive +Paper.book().write("task-queue", queue); // LinkedList +Paper.book().write("countries", countryCodeMap); // HashMap ``` #### Read Read data objects. Paper instantiates exactly the classes which has been used in saved data. The limited backward and forward compatibility is supported. See [Handle data class changes](#handle-data-structure-changes). ```java -String city = Paper.get("city"); -LinkedList queue = Paper.get("task-queue"); -HashMap countryCodeMap = Paper.get("countries"); +String city = Paper.book().read("city"); +LinkedList queue = Paper.book().read("task-queue"); +HashMap countryCodeMap = Paper.book().read("countries"); ``` Use default values if object doesn't exist in the storage. ```java -String city = Paper.get("city", "Kyiv"); -LinkedList queue = Paper.get("task-queue", new LinkedList()); -HashMap countryCodeMap = Paper.get("countries", new HashMap()); +String city = Paper.book().read("city", "Kyiv"); +LinkedList queue = Paper.book().read("task-queue", new LinkedList()); +HashMap countryCodeMap = Paper.book().read("countries", new HashMap()); ``` #### Delete Delete data for one key. ```java -Paper.delete("countries"); +Paper.book().delete("countries"); ``` Completely clear Paper storage. Doesn't require to call init() before usage. ```java -Paper.clear(context); +Paper.book().clear(context); ``` #### Handle data structure changes @@ -113,7 +113,7 @@ Paper is based on the following assumptions: - Saved data on mobile are relatively small; - Random file access on flash storage is very fast. -So each data object is saved in separate file and put/get operations write/read whole file. +So each data object is saved in separate file and write/read operations write/read whole file. The [Kryo](https://github.com/EsotericSoftware/kryo) is used for object graph serialization and to provide data compatibility support. diff --git a/paperdb/src/androidTest/java/io/paperdb/CompatibilityTest.java b/paperdb/src/androidTest/java/io/paperdb/CompatibilityTest.java index 1fa07b0..b53d309 100644 --- a/paperdb/src/androidTest/java/io/paperdb/CompatibilityTest.java +++ b/paperdb/src/androidTest/java/io/paperdb/CompatibilityTest.java @@ -20,8 +20,8 @@ public class CompatibilityTest { @Before public void setUp() throws Exception { - Paper.clear(getTargetContext()); Paper.init(getTargetContext()); + Paper.book().destroy(); } @Test @@ -34,10 +34,10 @@ public void testChangeClass() testClass.timestamp = 123; // Save original class. Only class name is changed to TestClassNew - Paper.put("test", testClass); + Paper.book().write("test", testClass); // Read and instantiate a modified class TestClassNew based on saved data in TestClass - TestClassNew newTestClass = Paper.get("test"); + TestClassNew newTestClass = Paper.book().read("test"); // Check original value is restored despite new default value in TestClassNew assertThat(newTestClass.name).isEqualTo("original"); // Check default value for new added field @@ -51,9 +51,9 @@ public void testNotCompatibleClassChanges() throws Exception { TestClass testClass = getClassInstanceWithNewName(TestClass.class, TestClassNotCompatible.class.getName()); testClass.timestamp = 123; - Paper.put("not-compatible", testClass); + Paper.book().write("not-compatible", testClass); - Paper.get("not-compatible"); + Paper.book().read("not-compatible"); } @Test @@ -62,9 +62,9 @@ public void testTransientFields() throws Exception { tc.timestamp = 123; tc.transientField = "changed"; - Paper.put("transient-class", tc); + Paper.book().write("transient-class", tc); - TestClassTransient readTc = Paper.get("transient-class"); + TestClassTransient readTc = Paper.book().read("transient-class"); assertThat(readTc.timestamp).isEqualTo(123); assertThat(readTc.transientField).isEqualTo("default"); } diff --git a/paperdb/src/androidTest/java/io/paperdb/DataTest.java b/paperdb/src/androidTest/java/io/paperdb/DataTest.java index dc89c14..f4f064b 100644 --- a/paperdb/src/androidTest/java/io/paperdb/DataTest.java +++ b/paperdb/src/androidTest/java/io/paperdb/DataTest.java @@ -23,47 +23,47 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests List put/get API + * Tests List write/read API */ @RunWith(AndroidJUnit4.class) public class DataTest { @Before public void setUp() throws Exception { - Paper.clear(getTargetContext()); Paper.init(getTargetContext()); + Paper.book().destroy(); } @Test public void testPutEmptyList() throws Exception { final List inserted = genPersonList(0); - Paper.put("persons", inserted); - assertThat(Paper.get("persons")).isEmpty(); + Paper.book().write("persons", inserted); + assertThat(Paper.book().read("persons")).isEmpty(); } @Test public void testPutGetList() { final List inserted = genPersonList(10000); - Paper.put("persons", inserted); - List persons = Paper.get("persons"); + Paper.book().write("persons", inserted); + List persons = Paper.book().read("persons"); assertThat(persons).isEqualTo(inserted); } @Test public void testPutMap() { final Map inserted = genPersonMap(10000); - Paper.put("persons", inserted); + Paper.book().write("persons", inserted); - final Map personMap = Paper.get("persons"); + final Map personMap = Paper.book().read("persons"); assertThat(personMap).isEqualTo(inserted); } @Test public void testPutPOJO() { final Person person = genPerson(1); - Paper.put("profile", person); + Paper.book().write("profile", person); - final Person savedPerson = Paper.get("profile"); + final Person savedPerson = Paper.book().read("profile"); assertThat(savedPerson).isEqualTo(person); assertThat(savedPerson).isNotSameAs(person); } @@ -134,8 +134,8 @@ public void testPutSynchronizedList() { } private Object testReadWriteWithoutClassCheck(Object originObj) { - Paper.put("obj", originObj); - Object readObj = Paper.get("obj"); + Paper.book().write("obj", originObj); + Object readObj = Paper.book().read("obj"); assertThat(readObj).isEqualTo(originObj); return readObj; } diff --git a/paperdb/src/androidTest/java/io/paperdb/PaperTest.java b/paperdb/src/androidTest/java/io/paperdb/PaperTest.java index 11da586..0293b02 100644 --- a/paperdb/src/androidTest/java/io/paperdb/PaperTest.java +++ b/paperdb/src/androidTest/java/io/paperdb/PaperTest.java @@ -20,110 +20,117 @@ public class PaperTest { @Before public void setUp() throws Exception { - Paper.clear(getTargetContext()); Paper.init(getTargetContext()); + Paper.book().destroy(); } @Test public void testExist() throws Exception { - assertFalse(Paper.exist("persons")); - Paper.put("persons", TestDataGenerator.genPersonList(10)); - assertTrue(Paper.exist("persons")); + assertFalse(Paper.book().exist("persons")); + Paper.book().write("persons", TestDataGenerator.genPersonList(10)); + assertTrue(Paper.book().exist("persons")); } @Test public void testDelete() throws Exception { - Paper.put("persons", TestDataGenerator.genPersonList(10)); - assertTrue(Paper.exist("persons")); - Paper.delete("persons"); - assertFalse(Paper.exist("persons")); + Paper.book().write("persons", TestDataGenerator.genPersonList(10)); + assertTrue(Paper.book().exist("persons")); + Paper.book().delete("persons"); + assertFalse(Paper.book().exist("persons")); } @Test public void testDeleteNotExisted() throws Exception { - assertFalse(Paper.exist("persons")); - Paper.delete("persons"); + assertFalse(Paper.book().exist("persons")); + Paper.book().delete("persons"); } @Test public void testClear() throws Exception { - Paper.put("persons", TestDataGenerator.genPersonList(10)); - Paper.put("persons2", TestDataGenerator.genPersonList(20)); - assertTrue(Paper.exist("persons")); - assertTrue(Paper.exist("persons2")); + Paper.book().write("persons", TestDataGenerator.genPersonList(10)); + Paper.book().write("persons2", TestDataGenerator.genPersonList(20)); + assertTrue(Paper.book().exist("persons")); + assertTrue(Paper.book().exist("persons2")); - Paper.clear(getTargetContext()); + Paper.book().destroy(); // init() call is not required after clear() - assertFalse(Paper.exist("persons")); - assertFalse(Paper.exist("persons2")); + assertFalse(Paper.book().exist("persons")); + assertFalse(Paper.book().exist("persons2")); // Should be possible to continue to use Paper after clear() - Paper.put("persons3", TestDataGenerator.genPersonList(30)); - assertTrue(Paper.exist("persons3")); - assertThat(Paper.get("persons3")).hasSize(30); + Paper.book().write("persons3", TestDataGenerator.genPersonList(30)); + assertTrue(Paper.book().exist("persons3")); + assertThat(Paper.book().read("persons3")).hasSize(30); } @Test public void testPutGetNormal() { - Paper.put("city", "Lund"); - String val = Paper.get("city", "default"); + Paper.book().write("city", "Lund"); + String val = Paper.book().read("city", "default"); assertThat(val).isEqualTo("Lund"); } @Test public void testPutGetNormalAfterReinit() { - Paper.put("city", "Lund"); - String val = Paper.get("city", "default"); + Paper.book().write("city", "Lund"); + String val = Paper.book().read("city", "default"); Paper.init(getTargetContext());// Reinit Paper instance assertThat(val).isEqualTo("Lund"); } @Test public void testGetNotExisted() { - String val = Paper.get("non-existed"); + String val = Paper.book().read("non-existed"); assertThat(val).isNull(); } @Test public void testGetDefault() { - String val = Paper.get("non-existed", "default"); + String val = Paper.book().read("non-existed", "default"); assertThat(val).isEqualTo("default"); } @Test public void testPutNull() { - Paper.put("city", "Lund"); - String val = Paper.get("city"); + Paper.book().write("city", "Lund"); + String val = Paper.book().read("city"); assertThat(val).isEqualTo("Lund"); - Paper.put("city", null); - String nullVal = Paper.get("city"); + Paper.book().write("city", null); + String nullVal = Paper.book().read("city"); assertThat(nullVal).isNull(); } @Test public void testReplace() { - Paper.put("city", "Lund"); - Paper.put("city", "Kyiv"); - assertThat(Paper.get("city")).isEqualTo("Kyiv"); + Paper.book().write("city", "Lund"); + assertThat(Paper.book().read("city")).isEqualTo("Lund"); + Paper.book().write("city", "Kyiv"); + assertThat(Paper.book().read("city")).isEqualTo("Kyiv"); } @Test public void testValidKeyNames() { - Paper.put("city", "Lund"); - assertThat(Paper.get("city")).isEqualTo("Lund"); + Paper.book().write("city", "Lund"); + assertThat(Paper.book().read("city")).isEqualTo("Lund"); - Paper.put("city.dasd&%", "Lund"); - assertThat(Paper.get("city.dasd&%")).isEqualTo("Lund"); + Paper.book().write("city.dasd&%", "Lund"); + assertThat(Paper.book().read("city.dasd&%")).isEqualTo("Lund"); - Paper.put("city-ads", "Lund"); - assertThat(Paper.get("city-ads")).isEqualTo("Lund"); + Paper.book().write("city-ads", "Lund"); + assertThat(Paper.book().read("city-ads")).isEqualTo("Lund"); } @Test(expected=PaperDbException.class) public void testInvalidKeyNameBackslash() { - Paper.put("city/ads", "Lund"); - assertThat(Paper.get("city/ads")).isEqualTo("Lund"); + Paper.book().write("city/ads", "Lund"); + assertThat(Paper.book().read("city/ads")).isEqualTo("Lund"); } + @Test(expected=PaperDbException.class) + public void testBookname() { + Paper.book().write("city", "Lund"); + assertThat(Paper.book().read("city")).isEqualTo("Lund"); + Paper.book(Paper.DEFAULT_DB_NAME).write("city", "Lund"); + } } \ No newline at end of file diff --git a/paperdb/src/androidTest/java/io/paperdb/benchmark/Benchmark.java b/paperdb/src/androidTest/java/io/paperdb/benchmark/Benchmark.java index 8ecc2a6..1d418a1 100644 --- a/paperdb/src/androidTest/java/io/paperdb/benchmark/Benchmark.java +++ b/paperdb/src/androidTest/java/io/paperdb/benchmark/Benchmark.java @@ -31,8 +31,8 @@ public class Benchmark extends AndroidTestCase { @Test public void testReadWrite500Contacts() throws Exception { final List contacts = TestDataGenerator.genPersonList(500); - Paper.clear(getTargetContext()); Paper.init(getTargetContext()); + Paper.book().destroy(); long paperTime = runTest(new PaperReadWriteContactsTest(), contacts, REPEAT_COUNT); Hawk.init(getTargetContext()); @@ -45,8 +45,8 @@ public void testReadWrite500Contacts() throws Exception { @Test public void testWrite500Contacts() throws Exception { final List contacts = TestDataGenerator.genPersonList(500); - Paper.clear(getTargetContext()); Paper.init(getTargetContext()); + Paper.book().destroy(); long paperTime = runTest(new PaperWriteContactsTest(), contacts, REPEAT_COUNT); Hawk.init(getTargetContext()); @@ -59,8 +59,8 @@ public void testWrite500Contacts() throws Exception { @Test public void testRead500Contacts() throws Exception { final List contacts = TestDataGenerator.genPersonList(500); - Paper.clear(getTargetContext()); Paper.init(getTargetContext()); + Paper.book().destroy(); runTest(new PaperWriteContactsTest(), contacts, REPEAT_COUNT); //Prepare long paperTime = runTest(new PaperReadContactsTest(), contacts, REPEAT_COUNT); @@ -93,8 +93,8 @@ private class PaperReadWriteContactsTest implements TestTask> { @Override public void run(int i, List extra) { String key = "contacts" + i; - Paper.put(key, extra); - Paper.>get(key); + Paper.book().write(key, extra); + Paper.book().>read(key); } } @@ -111,7 +111,7 @@ private class PaperWriteContactsTest implements TestTask> { @Override public void run(int i, List extra) { String key = "contacts" + i; - Paper.put(key, extra); + Paper.book().write(key, extra); } } @@ -127,7 +127,7 @@ private class PaperReadContactsTest implements TestTask> { @Override public void run(int i, List extra) { String key = "contacts" + i; - Paper.>get(key); + Paper.book().>read(key); } } diff --git a/paperdb/src/androidTest/java/io/paperdb/multithread/MultiThreadTest.java b/paperdb/src/androidTest/java/io/paperdb/multithread/MultiThreadTest.java index 0b9ed93..d2f6067 100644 --- a/paperdb/src/androidTest/java/io/paperdb/multithread/MultiThreadTest.java +++ b/paperdb/src/androidTest/java/io/paperdb/multithread/MultiThreadTest.java @@ -43,7 +43,7 @@ private Runnable getInsertRunnable() { public void run() { int size = new Random().nextInt(200); final List inserted100 = TestDataGenerator.genPersonList(size); - Paper.put("persons", inserted100); + Paper.book().write("persons", inserted100); } }; } @@ -52,7 +52,7 @@ private Runnable getSelectRunnable() { return new Runnable() { @Override public void run() { - Paper.get("persons"); + Paper.book().read("persons"); } }; } diff --git a/paperdb/src/main/java/io/paperdb/Book.java b/paperdb/src/main/java/io/paperdb/Book.java new file mode 100644 index 0000000..7879ace --- /dev/null +++ b/paperdb/src/main/java/io/paperdb/Book.java @@ -0,0 +1,86 @@ +package io.paperdb; + +import android.content.Context; + +public class Book { + + private final Storage mStorage; + + protected Book(Context context, String dbName) { + mStorage = new DbStoragePlainFile(context.getApplicationContext(), dbName); + } + + /** + * Destroys all data saved in Book. + */ + public void destroy() { + mStorage.destroy(); + } + + /** + * Saves any types of POJOs or collections in Book storage. + * + * @param key object key is used as part of object's file name + * @param value object to save, must have no-arg constructor + * @param object type + * @return this Book instance + */ + public Book write(String key, T value) { + if (value == null) { + mStorage.deleteIfExists(key); + } else { + mStorage.insert(key, value); + } + return this; + } + + /** + * Instantiates saved object using original object class (e.g. LinkedList). Support limited + * backward and forward compatibility: removed fields are ignored, new fields have their + * default values. + *

+ * All instantiated objects must have no-arg constructors. + * + * @param key object key to read + * @return the saved object instance or null + */ + public T read(String key) { + return read(key, null); + } + + /** + * Instantiates saved object using original object class (e.g. LinkedList). Support limited + * backward and forward compatibility: removed fields are ignored, new fields have their + * default values. + *

+ * All instantiated objects must have no-arg constructors. + * + * @param key object key to read + * @param defaultValue will be returned if key doesn't exist + * @return the saved object instance or null + */ + public T read(String key, T defaultValue) { + T value = mStorage.select(key); + return value == null ? defaultValue : value; + } + + /** + * Check if an object with the given key is saved in Book storage. + * + * @param key object key + * @return true if object with given key exists in Book storage, false otherwise + */ + public boolean exist(String key) { + return mStorage.exist(key); + } + + /** + * Delete saved object for given key if it is exist. + * + * @param key object key + */ + public void delete(String key) { + mStorage.deleteIfExists(key); + } + +} diff --git a/paperdb/src/main/java/io/paperdb/DbStoragePlainFile.java b/paperdb/src/main/java/io/paperdb/DbStoragePlainFile.java index f846658..81fef78 100644 --- a/paperdb/src/main/java/io/paperdb/DbStoragePlainFile.java +++ b/paperdb/src/main/java/io/paperdb/DbStoragePlainFile.java @@ -153,7 +153,6 @@ public synchronized void deleteIfExists(String key) { } private File getOriginalFile(String key) { - //TODO check valid file name/path with regexp final String tablePath = mFilesDir + File.separator + key + ".pt"; return new File(tablePath); } diff --git a/paperdb/src/main/java/io/paperdb/Paper.java b/paperdb/src/main/java/io/paperdb/Paper.java index ad324be..18fe8f1 100644 --- a/paperdb/src/main/java/io/paperdb/Paper.java +++ b/paperdb/src/main/java/io/paperdb/Paper.java @@ -4,6 +4,8 @@ import android.content.Context; import android.os.Bundle; +import java.util.concurrent.ConcurrentHashMap; + /** * Fast NoSQL data storage with auto-upgrade support to save any types of Plain Old Java Objects or * collections using Kryo serialization. @@ -19,113 +21,53 @@ public class Paper { static final String TAG = "paperdb"; - private static final String DEFAULT_DB_NAME = "io.paperdb"; + static final String DEFAULT_DB_NAME = "io.paperdb"; - private static Paper INSTANCE; + private static Context mContext; - private final Storage mStorage; + private static ConcurrentHashMap mBookMap = new ConcurrentHashMap<>(); /** * Lightweight method to init Paper instance. Should be executed in {@link Application#onCreate()} * or {@link android.app.Activity#onCreate(Bundle)}. *

- * All {@link #put(String, Object)} and {@link #get(String)} methods should be called after this - * method is executed. * - * @param context context, uses to get application context + * @param context context, used to get application context */ public static void init(Context context) { - INSTANCE = new Paper(context); - } - - /** - * Clears all data saved by Paper. Can be used even when Paper yet not initialized - * by {@link #init(Context)} - * - * @param context context - */ - public static void clear(Context context) { - if (INSTANCE == null) { - new Paper(context).mStorage.destroy(); - } else { - INSTANCE.mStorage.destroy(); - } - } - - /** - * Saves any types of POJOs or collections in Paper storage. - * - * @param key object key is used as part of object's file name - * @param value object to save, must have no-arg constructor - * @param object type - * @return this Paper instance - */ - public static Paper put(String key, T value) { - if (value == null) { - INSTANCE.mStorage.deleteIfExists(key); - } else { - INSTANCE.mStorage.insert(key, value); - } - return INSTANCE; - } - - /** - * Instantiates saved object using original object class (e.g. LinkedList). Support limited - * backward and forward compatibility: removed fields are ignored, new fields have their - * default values. - *

- * All instantiated objects must have no-arg constructors. - * - * @param key object key to read - * @return the saved object instance or null - */ - public static T get(String key) { - return get(key, null); + mContext = context.getApplicationContext(); } /** - * Instantiates saved object using original object class (e.g. LinkedList). Support limited - * backward and forward compatibility: removed fields are ignored, new fields have their - * default values. - *

- * All instantiated objects must have no-arg constructors. + * This method will create new book paper instance for specific name * - * @param key object key to read - * @param defaultValue will be returned if key doesn't exist - * @return the saved object instance or null + * @param name name of new database + * @return Paper instance */ - public static T get(String key, T defaultValue) { - T value = INSTANCE.mStorage.select(key); - return value == null ? defaultValue : value; + public static Book book(String name) { + if (name.equals(DEFAULT_DB_NAME)) throw new PaperDbException(DEFAULT_DB_NAME + + " name is reserved for default library name"); + return getBook(name); } /** - * Check if an object with the given key is saved in Paper storage. + * This method will create new book paper instance for specific name * - * @param key object key - * @return true if object with given key exists in Paper storage, false otherwise + * @return Book instance */ - public static boolean exist(String key) { - return INSTANCE.mStorage.exist(key); + public static Book book() { + return getBook(DEFAULT_DB_NAME); } - /** - * Delete saved object for given key if it is exist. - * - * @param key object key - * @return this Paper instance - */ - public static Paper delete(String key) { - INSTANCE.mStorage.deleteIfExists(key); - return INSTANCE; - } - - private Paper(Context context) { - this(context, DEFAULT_DB_NAME); - } - - private Paper(Context context, String dbName) { - mStorage = new DbStoragePlainFile(context.getApplicationContext(), dbName); + private static Book getBook(String name) { + if (mContext == null) { + throw new PaperDbException("Paper.init is not called"); + } + Book book = mBookMap.get(name); + if (book == null) { + book = new Book(mContext, name); + mBookMap.put(name, book); + } + return book; } - }