diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dc87f79 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +Change Log +========== + +Version 1.5 *(2016-04-28)* +---------------------------- + + * Save all the things! No more restriction to use classes only having no-arg constructor. + * Custom serializers can be added using `Paper.addSerializer()`. + * Kotlin is fully supported now, including saving `data class`es. Saving lambdas is not supported. + + +Version 1.1 *(2015-11-27)* +---------------------------- + + * New ```Paper.book().getAllKeys()``` api + * Proguard config for lib itself is included in aar. + + +Version 1.0 *(2015-09-15)* +---------------------------- + + * New multi-book API. + * 0.9 API is still supported and marked as deprecated. + * Unsafe possibility to write null values is disabled. + + *NOTE:* Data storage format is unchanged. You can easily use files created within version 0.9. \ No newline at end of file diff --git a/README.md b/README.md index 1fe97cb..1ee2ae5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ # Paper [![Build Status](https://travis-ci.org/pilgr/Paper.svg?branch=master)](https://travis-ci.org/pilgr/Paper) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Paper-blue.svg?style=flat)](http://android-arsenal.com/details/1/2080) -Paper is a [fast](#benchmark-results) NoSQL data storage for Android that lets you save/restore Java objects by using efficient Kryo serialization and handling data structure changes automatically. +Paper is a [fast](#benchmark-results) NoSQL data storage for Android that lets you save/restore Java/Kotlin objects using efficient Kryo serialization. Object structure changes handled automatically. ![Paper icon](/paper_icon.png) +#### What's [new](/CHANGELOG.md) in 1.5 +* Save all the things! No more restriction to use classes only having no-arg constructor. +* Custom serializers can be added using `Paper.addSerializer()`. +* Kotlin is fully supported now, including saving `data class`es. Obviously saving lambdas is not supported. + #### Add dependency ```groovy -compile 'io.paperdb:paperdb:1.1' +compile 'io.paperdb:paperdb:1.5' ``` #### Initialize Paper @@ -20,7 +25,7 @@ Paper.init(context); It's OK to call it in UI thread. All other methods should be used in background thread. #### Save -Save data object. **Your custom classes must have no-arg constructor.** +Save data object. Paper creates separate data file for each key. ```java diff --git a/build.gradle b/build.gradle index 5cfc154..99b836a 100644 --- a/build.gradle +++ b/build.gradle @@ -5,9 +5,9 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0' - classpath 'com.github.dcendents:android-maven-plugin:1.2' + classpath 'com.android.tools.build:gradle:2.0.0' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6' + classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..686b8df --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c71e76..fdb8024 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip diff --git a/paperdb/build.gradle b/paperdb/build.gradle index 682b014..091745b 100644 --- a/paperdb/build.gradle +++ b/paperdb/build.gradle @@ -1,4 +1,18 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +buildscript { + ext.kotlin_version = '1.0.1-2' + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} +repositories { + mavenCentral() +} android { compileSdkVersion 21 @@ -28,6 +42,18 @@ android { packagingOptions { exclude 'LICENSE.txt' } + sourceSets { + test.java.srcDirs += 'src/androidTest/kotlin' + } +} + +afterEvaluate { + android.sourceSets.all { sourceSet -> + if (!sourceSet.name.startsWith("androidTest")) + { + sourceSet.kotlin.setSrcDirs([]) + } + } } dependencies { @@ -37,6 +63,8 @@ dependencies { androidTestCompile 'com.orhanobut:hawk:1.14' androidTestCompile 'com.android.support.test:runner:0.3' androidTestCompile 'com.squareup.assertj:assertj-android:1.0.0' + androidTestCompile 'joda-time:joda-time:2.9.1' + androidTestCompile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } apply from: '../publish.gradle' diff --git a/paperdb/src/androidTest/java/io/paperdb/DataTest.java b/paperdb/src/androidTest/java/io/paperdb/DataTest.java index 815de0d..ec29cd8 100644 --- a/paperdb/src/androidTest/java/io/paperdb/DataTest.java +++ b/paperdb/src/androidTest/java/io/paperdb/DataTest.java @@ -14,8 +14,8 @@ import java.util.List; import java.util.Map; -import io.paperdb.testdata.ClassWithoutPublicNoArgConstructor; import io.paperdb.testdata.Person; +import io.paperdb.testdata.PersonArg; import static android.support.test.InstrumentationRegistry.getTargetContext; import static io.paperdb.testdata.TestDataGenerator.genPerson; @@ -61,7 +61,7 @@ public void testPutMap() { @Test public void testPutPOJO() { - final Person person = genPerson(1); + final Person person = genPerson(new Person(), 1); Paper.book().write("profile", person); final Person savedPerson = Paper.book().read("profile"); @@ -134,9 +134,9 @@ public void testPutSynchronizedList() { testReadWrite(Collections.synchronizedList(new ArrayList<>())); } - @Test(expected = PaperDbException.class) + @Test() public void testReadWriteClassWithoutNoArgConstructor() { - testReadWrite(new ClassWithoutPublicNoArgConstructor("constructor argument")); + testReadWrite(new PersonArg("name")); } private Object testReadWriteWithoutClassCheck(Object originObj) { diff --git a/paperdb/src/androidTest/java/io/paperdb/PaperTest.java b/paperdb/src/androidTest/java/io/paperdb/PaperTest.java index 6b9ac23..2e6af84 100644 --- a/paperdb/src/androidTest/java/io/paperdb/PaperTest.java +++ b/paperdb/src/androidTest/java/io/paperdb/PaperTest.java @@ -2,15 +2,19 @@ import android.support.test.runner.AndroidJUnit4; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; +import de.javakaffee.kryoserializers.jodatime.JodaDateTimeSerializer; import io.paperdb.testdata.TestDataGenerator; import static android.support.test.InstrumentationRegistry.getTargetContext; +import static junit.framework.Assert.assertEquals; import static junit.framework.TestCase.assertTrue; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertFalse; @@ -168,4 +172,12 @@ public void testGetAllKeys() { assertThat(allKeys.contains("city2")).isTrue(); } + @Test + public void testCustomSerializer() { + Paper.addSerializer(DateTime.class, new JodaDateTimeSerializer()); + + DateTime now = DateTime.now(DateTimeZone.UTC); + Paper.book("custom").write("joda-datetime", now); + assertEquals(Paper.book("custom").read("joda-datetime"), now); + } } \ 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 1d418a1..7956e2b 100644 --- a/paperdb/src/androidTest/java/io/paperdb/benchmark/Benchmark.java +++ b/paperdb/src/androidTest/java/io/paperdb/benchmark/Benchmark.java @@ -15,6 +15,7 @@ import io.paperdb.Paper; import io.paperdb.testdata.Person; +import io.paperdb.testdata.PersonArg; import io.paperdb.testdata.TestDataGenerator; import static android.support.test.InstrumentationRegistry.getTargetContext; @@ -39,7 +40,12 @@ public void testReadWrite500Contacts() throws Exception { Hawk.clear(); long hawkTime = runTest(new HawkReadWriteContactsTest(), contacts, REPEAT_COUNT); - printResults("Read/write 500 contacts", paperTime, hawkTime); + final List contactsArg = TestDataGenerator.genPersonArgList(500); + Paper.init(getTargetContext()); + Paper.book().destroy(); + long paperArg = runTest(new PaperReadWriteContactsArgTest(), contactsArg, REPEAT_COUNT); + + printResults("Read/write 500 contacts", paperTime, hawkTime, paperArg); } @Test @@ -77,6 +83,12 @@ private void printResults(String name, long paperTime, long hawkTime) { name, paperTime, hawkTime)); } + private void printResults(String name, long paperTime, long hawkTime, long paperArgTime) { + Log.i(TAG, String.format("..................................\n%s " + + "\n Paper: %d \n Paper(arg-cons): %d \n Hawk: %d", + name, paperTime, paperArgTime, hawkTime)); + } + private long runTest(TestTask task, T extra, int repeat) { long start = SystemClock.uptimeMillis(); for (int i = 0; i < repeat; i++) { @@ -98,6 +110,15 @@ public void run(int i, List extra) { } } + private class PaperReadWriteContactsArgTest implements TestTask> { + @Override + public void run(int i, List extra) { + String key = "contacts" + i; + Paper.book().write(key, extra); + Paper.book().>read(key); + } + } + private class HawkReadWriteContactsTest implements TestTask> { @Override public void run(int i, List extra) { diff --git a/paperdb/src/androidTest/java/io/paperdb/deprecated/DataTest.java b/paperdb/src/androidTest/java/io/paperdb/deprecated/DataTest.java index 787573f..d06d417 100644 --- a/paperdb/src/androidTest/java/io/paperdb/deprecated/DataTest.java +++ b/paperdb/src/androidTest/java/io/paperdb/deprecated/DataTest.java @@ -26,6 +26,7 @@ /** * Tests deprecated put/get API */ +@SuppressWarnings("deprecation") @RunWith(AndroidJUnit4.class) public class DataTest { @@ -61,7 +62,7 @@ public void testPutMap() { @Test public void testPutPOJO() { - final Person person = genPerson(1); + final Person person = genPerson(new Person(), 1); Paper.put("profile", person); final Person savedPerson = Paper.get("profile"); diff --git a/paperdb/src/androidTest/java/io/paperdb/testdata/ClassWithoutPublicNoArgConstructor.java b/paperdb/src/androidTest/java/io/paperdb/testdata/ClassWithoutPublicNoArgConstructor.java deleted file mode 100644 index 233a754..0000000 --- a/paperdb/src/androidTest/java/io/paperdb/testdata/ClassWithoutPublicNoArgConstructor.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.paperdb.testdata; - -public class ClassWithoutPublicNoArgConstructor { - - public ClassWithoutPublicNoArgConstructor(@SuppressWarnings("UnusedParameters") String name) { - } - -} diff --git a/paperdb/src/androidTest/java/io/paperdb/testdata/PersonArg.java b/paperdb/src/androidTest/java/io/paperdb/testdata/PersonArg.java new file mode 100644 index 0000000..7228fff --- /dev/null +++ b/paperdb/src/androidTest/java/io/paperdb/testdata/PersonArg.java @@ -0,0 +1,36 @@ +package io.paperdb.testdata; + +import java.util.Arrays; + +// Person + arg constructor +public class PersonArg extends Person { + public PersonArg(String name) { + super(); + setName("changed" + name); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || PersonArg.class != o.getClass()) return false; + + PersonArg person = (PersonArg) o; + + if (getAge() != person.getAge()) return false; + if (!Arrays.equals(getBikes(), person.getBikes())) return false; + if (getName() != null ? !getName().equals(person.getName()) : person.getName() != null) + return false; + //noinspection RedundantIfStatement + if (getPhoneNumbers() != null + ? !getPhoneNumbers().equals(person.getPhoneNumbers()) + : person.getPhoneNumbers() != null) + return false; + + return true; + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/paperdb/src/androidTest/java/io/paperdb/testdata/TestDataGenerator.java b/paperdb/src/androidTest/java/io/paperdb/testdata/TestDataGenerator.java index 60577a7..6280f58 100644 --- a/paperdb/src/androidTest/java/io/paperdb/testdata/TestDataGenerator.java +++ b/paperdb/src/androidTest/java/io/paperdb/testdata/TestDataGenerator.java @@ -11,15 +11,23 @@ public class TestDataGenerator { public static List genPersonList(int size) { List list = new ArrayList<>(); for (int i = 0; i < size; i++) { - Person p = genPerson(i); + Person p = genPerson(new Person(), i); + list.add(p); + } + return list; + } + + public static List genPersonArgList(int size) { + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + PersonArg p = genPerson(new PersonArg("name"), i); list.add(p); } return list; } @NonNull - public static Person genPerson(int i) { - Person p = new Person(); + public static T genPerson(T p, int i) { p.setAge(i); p.setBikes(new String[2]); p.getBikes()[0] = "Kellys gen#" + i; diff --git a/paperdb/src/androidTest/kotlin/io/paperdb/KotlinCompatibilityTest.kt b/paperdb/src/androidTest/kotlin/io/paperdb/KotlinCompatibilityTest.kt new file mode 100644 index 0000000..a074da7 --- /dev/null +++ b/paperdb/src/androidTest/kotlin/io/paperdb/KotlinCompatibilityTest.kt @@ -0,0 +1,97 @@ +package io.paperdb + +import android.support.test.InstrumentationRegistry.getTargetContext +import android.support.test.runner.AndroidJUnit4 +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class KotlinCompatibilityTest { + + @Before + @Throws(Exception::class) + fun setUp() { + Paper.init(getTargetContext()) + Paper.book().destroy() + } + + @Test + fun testNormalClasses() { + class PersonNormalClass(val name: String = "name", val age: Int){ + override fun equals(other: Any?): Boolean{ + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as PersonNormalClass + + if (name != other.name) return false + if (age != other.age) return false + + return true + } + + override fun hashCode(): Int{ + var result = name.hashCode() + result += 31 * result + age + return result + } + } + + testReadWrite(PersonNormalClass("name", age = 42)) + testReadWrite(PersonNormalClass(age = 42)) + } + + @Test + fun testDataClasses() { + data class PersonDataClass(val name: String = "name", val age: Int) + + testReadWrite(PersonDataClass("Julia", age = 42)) + testReadWrite(PersonDataClass(age = 42)) + } + + @Test + fun testClassesWithLambda() { + class PersonWithLambda(val block: (PersonWithLambda.() -> Unit)? = null) { + init { + block?.invoke(this) + } + + var name: String? = null + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as PersonWithLambda + + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + return name?.hashCode() ?: 0 + } + + } + + testReadWrite(PersonWithLambda()) + testReadWrite(PersonWithLambda().apply { name = "new-name" }) + testReadWrite(PersonWithLambda { name = "new-name" }) + } + + private fun testReadWriteWithoutClassCheck(originObj: Any): Any { + Paper.book().write("obj", originObj) + val readObj = Paper.book().read("obj") + assertThat(readObj).isEqualTo(originObj) + return readObj + } + + private fun testReadWrite(originObj: Any) { + val readObj = testReadWriteWithoutClassCheck(originObj) + assertThat(readObj.javaClass).isEqualTo(originObj.javaClass) + } + +} diff --git a/paperdb/src/main/java/io/paperdb/Book.java b/paperdb/src/main/java/io/paperdb/Book.java index 5429e3e..fd3c887 100644 --- a/paperdb/src/main/java/io/paperdb/Book.java +++ b/paperdb/src/main/java/io/paperdb/Book.java @@ -1,15 +1,17 @@ package io.paperdb; import android.content.Context; +import com.esotericsoftware.kryo.Serializer; +import java.util.HashMap; import java.util.List; public class Book { private final Storage mStorage; - protected Book(Context context, String dbName) { - mStorage = new DbStoragePlainFile(context.getApplicationContext(), dbName); + protected Book(Context context, String dbName, HashMap serializers) { + mStorage = new DbStoragePlainFile(context.getApplicationContext(), dbName, serializers); } /** diff --git a/paperdb/src/main/java/io/paperdb/DbStoragePlainFile.java b/paperdb/src/main/java/io/paperdb/DbStoragePlainFile.java index dc3dd6e..c8d126b 100644 --- a/paperdb/src/main/java/io/paperdb/DbStoragePlainFile.java +++ b/paperdb/src/main/java/io/paperdb/DbStoragePlainFile.java @@ -5,10 +5,13 @@ import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.KryoException; +import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer; +import org.objenesis.strategy.StdInstantiatorStrategy; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -16,12 +19,14 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.UUID; import de.javakaffee.kryoserializers.ArraysAsListSerializer; import de.javakaffee.kryoserializers.SynchronizedCollectionsSerializer; +import de.javakaffee.kryoserializers.UUIDSerializer; import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer; import io.paperdb.serializer.NoArgCollectionSerializer; @@ -31,6 +36,7 @@ public class DbStoragePlainFile implements Storage { private final Context mContext; private final String mDbName; + private final HashMap mCustomSerializers; private String mFilesDir; private boolean mPaperDirIsCreated; @@ -65,12 +71,23 @@ private Kryo createKryoInstance() { new NoArgCollectionSerializer()); // To keep backward compatibility don't change the order of serializers above + // UUID support + kryo.register(UUID.class, new UUIDSerializer()); + + for (Class clazz : mCustomSerializers.keySet()) + kryo.register(clazz, mCustomSerializers.get(clazz)); + + kryo.setInstantiatorStrategy( + new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy())); + return kryo; } - public DbStoragePlainFile(Context context, String dbName) { + public DbStoragePlainFile(Context context, String dbName, + HashMap serializers) { mContext = context; mDbName = dbName; + mCustomSerializers = serializers; } @Override diff --git a/paperdb/src/main/java/io/paperdb/Paper.java b/paperdb/src/main/java/io/paperdb/Paper.java index 05e03c0..6ea5650 100644 --- a/paperdb/src/main/java/io/paperdb/Paper.java +++ b/paperdb/src/main/java/io/paperdb/Paper.java @@ -4,6 +4,9 @@ import android.content.Context; import android.os.Bundle; +import com.esotericsoftware.kryo.Serializer; + +import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; /** @@ -26,6 +29,7 @@ public class Paper { private static Context mContext; private static final ConcurrentHashMap mBookMap = new ConcurrentHashMap<>(); + private static final HashMap mCustomSerializers = new HashMap<>(); /** * Lightweight method to init Paper instance. Should be executed in {@link Application#onCreate()} @@ -66,7 +70,7 @@ private static Book getBook(String name) { synchronized (mBookMap) { Book book = mBookMap.get(name); if (book == null) { - book = new Book(mContext, name); + book = new Book(mContext, name, mCustomSerializers); mBookMap.put(name, book); } return book; @@ -116,4 +120,17 @@ public static void clear(Context context) { init(context); book().destroy(); } + + /** + * Adds a custom serializer for a specific class + * When used, must be called right after Paper.init() + * + * @param clazz type of the custom serializer + * @param serializer the serializer instance + * @param type of the serializer + */ + public static void addSerializer(Class clazz, Serializer serializer) { + if (!mCustomSerializers.containsKey(clazz)) + mCustomSerializers.put(clazz, serializer); + } } diff --git a/publish.gradle b/publish.gradle index a85a6bc..ed788d5 100644 --- a/publish.gradle +++ b/publish.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.github.dcendents.android-maven' apply plugin: 'com.jfrog.bintray' // This is the library version used when deploying the artifact -version = "1.1" +version = "1.5" def siteUrl = 'https://github.com/pilgr/Paper' // Homepage URL of the library def gitUrl = 'https://github.com/pilgr/Paper.git' // Git repository URL