From 93282c33f64a5cce8ec4c57fe9163277819cb5d7 Mon Sep 17 00:00:00 2001 From: luoxiong <382060748@qq.com> Date: Thu, 26 Jan 2017 20:51:31 +0800 Subject: [PATCH] 11 --- .gitignore | 41 + app/.gitignore | 1 + app/build.gradle | 30 + app/proguard-rules.pro | 17 + .../mygreendao/ExampleInstrumentedTest.java | 26 + app/src/main/AndroidManifest.xml | 20 + .../java/com/mygreendao/MainActivity.java | 13 + app/src/main/res/layout/activity_main.xml | 18 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes app/src/main/res/values-w820dp/dimens.xml | 6 + app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 11 + .../java/com/mygreendao/ExampleUnitTest.java | 17 + build.gradle | 23 + gradle.properties | 17 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 +++ gradlew.bat | 90 ++ greenDao/.gitignore | 1 + greenDao/build.gradle | 35 + greenDao/libs/sqlcipher.jar | Bin 0 -> 99763 bytes greenDao/proguard-rules.pro | 17 + .../greendao/ExampleInstrumentedTest.java | 26 + greenDao/src/main/AndroidManifest.xml | 11 + .../org/greenrobot/greendao/AbstractDao.java | 1008 +++++++++++++++++ .../greendao/AbstractDaoMaster.java | 60 + .../greendao/AbstractDaoSession.java | 240 ++++ .../org/greenrobot/greendao/DaoException.java | 55 + .../java/org/greenrobot/greendao/DaoLog.java | 93 ++ .../java/org/greenrobot/greendao/DbUtils.java | 138 +++ .../greendao/InternalQueryDaoAccess.java | 52 + .../greendao/InternalUnitTestDaoAccess.java | 62 + .../org/greenrobot/greendao/Property.java | 120 ++ .../greendao/annotation/Convert.java | 24 + .../greendao/annotation/Entity.java | 62 + .../greendao/annotation/Generated.java | 19 + .../greenrobot/greendao/annotation/Id.java | 20 + .../greenrobot/greendao/annotation/Index.java | 35 + .../greendao/annotation/JoinEntity.java | 22 + .../greendao/annotation/JoinProperty.java | 20 + .../greenrobot/greendao/annotation/Keep.java | 24 + .../greendao/annotation/NotNull.java | 18 + .../greendao/annotation/OrderBy.java | 23 + .../greendao/annotation/Property.java | 19 + .../greendao/annotation/ToMany.java | 25 + .../greenrobot/greendao/annotation/ToOne.java | 25 + .../greendao/annotation/Transient.java | 15 + .../greendao/annotation/Unique.java | 26 + .../greendao/annotation/apihint/Beta.java | 34 + .../annotation/apihint/Experimental.java | 34 + .../greendao/annotation/apihint/Internal.java | 34 + .../greendao/async/AsyncDaoException.java | 41 + .../greendao/async/AsyncOperation.java | 228 ++++ .../async/AsyncOperationExecutor.java | 376 ++++++ .../async/AsyncOperationListener.java | 25 + .../greendao/async/AsyncSession.java | 350 ++++++ .../greendao/converter/PropertyConverter.java | 32 + .../greendao/database/Database.java | 46 + .../greendao/database/DatabaseOpenHelper.java | 198 ++++ .../greendao/database/DatabaseStatement.java | 41 + .../greendao/database/EncryptedDatabase.java | 88 ++ .../database/EncryptedDatabaseStatement.java | 84 ++ .../greendao/database/StandardDatabase.java | 88 ++ .../database/StandardDatabaseStatement.java | 83 ++ .../greendao/identityscope/IdentityScope.java | 53 + .../identityscope/IdentityScopeLong.java | 159 +++ .../identityscope/IdentityScopeObject.java | 142 +++ .../identityscope/IdentityScopeType.java | 20 + .../greendao/internal/DaoConfig.java | 177 +++ .../greendao/internal/FastCursor.java | 266 +++++ .../greendao/internal/LongHashMap.java | 166 +++ .../greendao/internal/SqlUtils.java | 167 +++ .../greendao/internal/TableStatements.java | 155 +++ .../greendao/query/AbstractQuery.java | 86 ++ .../greendao/query/AbstractQueryData.java | 89 ++ .../query/AbstractQueryWithLimit.java | 87 ++ .../greendao/query/CloseableListIterator.java | 31 + .../greenrobot/greendao/query/CountQuery.java | 71 ++ .../greendao/query/CursorQuery.java | 77 ++ .../greendao/query/DeleteQuery.java | 81 ++ .../org/greenrobot/greendao/query/Join.java | 91 ++ .../greenrobot/greendao/query/LazyList.java | 363 ++++++ .../org/greenrobot/greendao/query/Query.java | 192 ++++ .../greendao/query/QueryBuilder.java | 501 ++++++++ .../greendao/query/WhereCollector.java | 108 ++ .../greendao/query/WhereCondition.java | 168 +++ .../org/greenrobot/greendao/rx/RxBase.java | 72 ++ .../org/greenrobot/greendao/rx/RxDao.java | 401 +++++++ .../org/greenrobot/greendao/rx/RxQuery.java | 113 ++ .../greenrobot/greendao/rx/RxTransaction.java | 78 ++ .../org/greenrobot/greendao/rx/RxUtils.java | 61 + .../greendao/test/AbstractDaoSessionTest.java | 67 ++ .../greendao/test/AbstractDaoTest.java | 96 ++ .../greendao/test/AbstractDaoTestLongPk.java | 66 ++ .../test/AbstractDaoTestSinglePk.java | 372 ++++++ .../test/AbstractDaoTestStringPk.java | 48 + .../org/greenrobot/greendao/test/DbTest.java | 127 +++ greenDao/src/main/res/values/strings.xml | 3 + .../greenrobot/greendao/ExampleUnitTest.java | 17 + settings.gradle | 1 + 107 files changed, 9359 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/mygreendao/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/mygreendao/MainActivity.java create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/mygreendao/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 greenDao/.gitignore create mode 100644 greenDao/build.gradle create mode 100644 greenDao/libs/sqlcipher.jar create mode 100644 greenDao/proguard-rules.pro create mode 100644 greenDao/src/androidTest/java/org/greenrobot/greendao/ExampleInstrumentedTest.java create mode 100644 greenDao/src/main/AndroidManifest.xml create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/AbstractDao.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/AbstractDaoMaster.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/AbstractDaoSession.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/DaoException.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/DaoLog.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/DbUtils.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/InternalQueryDaoAccess.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/InternalUnitTestDaoAccess.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/Property.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Convert.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Entity.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Generated.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Id.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Index.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/JoinEntity.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/JoinProperty.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Keep.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/NotNull.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/OrderBy.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Property.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/ToMany.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/ToOne.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Transient.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/Unique.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Beta.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Experimental.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Internal.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/async/AsyncDaoException.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperation.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperationExecutor.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperationListener.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/async/AsyncSession.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/converter/PropertyConverter.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/database/Database.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/database/DatabaseOpenHelper.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/database/DatabaseStatement.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/database/EncryptedDatabase.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/database/EncryptedDatabaseStatement.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/database/StandardDatabase.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/database/StandardDatabaseStatement.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScope.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeLong.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeObject.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeType.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/internal/DaoConfig.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/internal/FastCursor.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/internal/LongHashMap.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/internal/SqlUtils.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/internal/TableStatements.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQuery.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQueryData.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQueryWithLimit.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/CloseableListIterator.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/CountQuery.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/CursorQuery.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/DeleteQuery.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/Join.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/LazyList.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/Query.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/QueryBuilder.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/WhereCollector.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/query/WhereCondition.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/rx/RxBase.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/rx/RxDao.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/rx/RxQuery.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/rx/RxTransaction.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/rx/RxUtils.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoSessionTest.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTest.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestLongPk.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestSinglePk.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestStringPk.java create mode 100644 greenDao/src/main/java/org/greenrobot/greendao/test/DbTest.java create mode 100644 greenDao/src/main/res/values/strings.xml create mode 100644 greenDao/src/test/java/org/greenrobot/greendao/ExampleUnitTest.java create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..908781e --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml +.idea/ + + +# Keystore files +*.jks \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..266f2cb --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "com.mygreendao" + minSdkVersion 14 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:25.1.0' + testCompile 'junit:junit:4.12' + compile project(':greenDao') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f7d6ea9 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\develop\Android\adt-bundle-windows-x86_64-20140702\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/com/mygreendao/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/mygreendao/ExampleInstrumentedTest.java new file mode 100644 index 0000000..bfc589d --- /dev/null +++ b/app/src/androidTest/java/com/mygreendao/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.mygreendao; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.mygreendao", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11e3504 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/mygreendao/MainActivity.java b/app/src/main/java/com/mygreendao/MainActivity.java new file mode 100644 index 0000000..9a4fed7 --- /dev/null +++ b/app/src/main/java/com/mygreendao/MainActivity.java @@ -0,0 +1,13 @@ +package com.mygreendao; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b691d55 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 zcmZ{nX*|@A^T0p5j$I+^%FVhdvMbgt%d+mG98ubwNv_tpITppba^GiieBBZGI>I89 zGgm8TA>_)DlEu&W;s3#ZUNiH4&CF{a%siTjzG;eOzQB6{003qKeT?}z_5U*{{kgZ; zdV@U&tqa-&4FGisjMN8o=P}$t-`oTM2oeB5d9mHPgTYJx4jup)+5a;Tke$m708DocFzDL>U$$}s6FGiy_I1?O zHXq`q884|^O4Q*%V#vwxqCz-#8i`Gu)2LeB0{%%VKunOF%9~JcFB9MM>N00M`E~;o zBU%)O5u-D6NF~OQV7TV#JAN;=Lylgxy0kncoQpGq<<_gxw`FC=C-cV#$L|(47Hatl ztq3Jngq00x#}HGW@_tj{&A?lwOwrVX4@d66vLVyj1H@i}VD2YXd)n03?U5?cKtFz4 zW#@+MLeDVP>fY0F2IzT;r5*MAJ2}P8Z{g3utX0<+ZdAC)Tvm-4uN!I7|BTw&G%RQn zR+A5VFx(}r<1q9^N40XzP=Jp?i=jlS7}T~tB4CsWx!XbiHSm zLu}yar%t>-3jlutK=wdZhES->*1X({YI;DN?6R=C*{1U6%wG`0>^?u}h0hhqns|SeTmV=s;Gxx5F9DtK>{>{f-`SpJ`dO26Ujk?^%ucsuCPe zIUk1(@I3D^7{@jmXO2@<84|}`tDjB}?S#k$ik;jC))BH8>8mQWmZ zF#V|$gW|Xc_wmmkoI-b5;4AWxkA>>0t4&&-eC-J_iP(tLT~c6*(ZnSFlhw%}0IbiJ ztgnrZwP{RBd(6Ds`dM~k;rNFgkbU&Yo$KR#q&%Kno^YXF5ONJwGwZ*wEr4wYkGiXs z$&?qX!H5sV*m%5t@3_>ijaS5hp#^Pu>N_9Q?2grdNp({IZnt|P9Xyh);q|BuoqeUJ zfk(AGX4odIVADHEmozF|I{9j>Vj^jCU}K)r>^%9#E#Y6B0i#f^iYsNA!b|kVS$*zE zx7+P?0{oudeZ2(ke=YEjn#+_cdu_``g9R95qet28SG>}@Me!D6&}un*e#CyvlURrg8d;i$&-0B?4{eYEgzwotp*DOQ_<=Ai21Kzb0u zegCN%3bdwxj!ZTLvBvexHmpTw{Z3GRGtvkwEoKB1?!#+6h1i2JR%4>vOkPN_6`J}N zk}zeyY3dPV+IAyn;zRtFH5e$Mx}V(|k+Ey#=nMg-4F#%h(*nDZDK=k1snlh~Pd3dA zV!$BoX_JfEGw^R6Q2kpdKD_e0m*NX?M5;)C zb3x+v?J1d#jRGr=*?(7Habkk1F_#72_iT7{IQFl<;hkqK83fA8Q8@(oS?WYuQd4z^ z)7eB?N01v=oS47`bBcBnKvI&)yS8`W8qHi(h2na?c6%t4mU(}H(n4MO zHIpFdsWql()UNTE8b=|ZzY*>$Z@O5m9QCnhOiM%)+P0S06prr6!VET%*HTeL4iu~!y$pN!mOo5t@1 z?$$q-!uP(+O-%7<+Zn5i=)2OftC+wOV;zAU8b`M5f))CrM6xu94e2s78i&zck@}%= zZq2l!$N8~@63!^|`{<=A&*fg;XN*7CndL&;zE(y+GZVs-IkK~}+5F`?ergDp=9x1w z0hkii!N(o!iiQr`k`^P2LvljczPcM`%7~2n#|K7nJq_e0Ew;UsXV_~3)<;L?K9$&D zUzgUOr{C6VLl{Aon}zp`+fH3>$*~swkjCw|e>_31G<=U0@B*~hIE)|WSb_MaE41Prxp-2eEg!gcon$fN6Ctl7A_lV8^@B9B+G~0=IYgc%VsprfC`e zoBn&O3O)3MraW#z{h3bWm;*HPbp*h+I*DoB%Y~(Fqp9+x;c>K2+niydO5&@E?SoiX_zf+cI09%%m$y=YMA~rg!xP*>k zmYxKS-|3r*n0J4y`Nt1eO@oyT0Xvj*E3ssVNZAqQnj-Uq{N_&3e45Gg5pna+r~Z6^ z>4PJ7r(gO~D0TctJQyMVyMIwmzw3rbM!};>C@8JA<&6j3+Y9zHUw?tT_-uNh^u@np zM?4qmcc4MZjY1mWLK!>1>7uZ*%Pe%=DV|skj)@OLYvwGXuYBoZvbB{@l}cHK!~UHm z4jV&m&uQAOLsZUYxORkW4|>9t3L@*ieU&b0$sAMH&tKidc%;nb4Z=)D7H<-`#%$^# zi`>amtzJ^^#zB2e%o*wF!gZBqML9>Hq9jqsl-|a}yD&JKsX{Op$7)_=CiZvqj;xN& zqb@L;#4xW$+icPN?@MB|{I!>6U(h!Wxa}14Z0S&y|A5$zbH(DXuE?~WrqNv^;x}vI z0PWfSUuL7Yy``H~*?|%z zT~ZWYq}{X;q*u-}CT;zc_NM|2MKT8)cMy|d>?i^^k)O*}hbEcCrU5Bk{Tjf1>$Q=@ zJ9=R}%vW$~GFV_PuXqE4!6AIuC?Tn~Z=m#Kbj3bUfpb82bxsJ=?2wL>EGp=wsj zAPVwM=CffcycEF; z@kPngVDwPM>T-Bj4##H9VONhbq%=SG;$AjQlV^HOH7!_vZk=}TMt*8qFI}bI=K9g$fgD9$! zO%cK1_+Wbk0Ph}E$BR2}4wO<_b0{qtIA1ll>s*2^!7d2e`Y>$!z54Z4FmZ*vyO}EP z@p&MG_C_?XiKBaP#_XrmRYszF;Hyz#2xqG%yr991pez^qN!~gT_Jc=PPCq^8V(Y9K zz33S+Mzi#$R}ncqe!oJ3>{gacj44kx(SOuC%^9~vT}%7itrC3b;ZPfX;R`D2AlGgN zw$o4-F77!eWU0$?^MhG9zxO@&zDcF;@w2beXEa3SL^htWYY{5k?ywyq7u&)~Nys;@ z8ZNIzUw$#ci&^bZ9mp@A;7y^*XpdWlzy%auO1hU=UfNvfHtiPM@+99# z!uo2`>!*MzphecTjN4x6H)xLeeDVEO#@1oDp`*QsBvmky=JpY@fC0$yIexO%f>c-O zAzUA{ch#N&l;RClb~;`@dqeLPh?e-Mr)T-*?Sr{32|n(}m>4}4c3_H3*U&Yj)grth z{%F0z7YPyjux9hfqa+J|`Y%4gwrZ_TZCQq~0wUR8}9@Jj4lh( z#~%AcbKZ++&f1e^G8LPQ)*Yy?lp5^z4pDTI@b^hlv06?GC%{ZywJcy}3U@zS3|M{M zGPp|cq4Zu~9o_cEZiiNyU*tc73=#Mf>7uzue|6Qo_e!U;oJ)Z$DP~(hOcRy&hR{`J zP7cNIgc)F%E2?p%{%&sxXGDb0yF#zac5fr2x>b)NZz8prv~HBhw^q=R$nZ~@&zdBi z)cEDu+cc1?-;ZLm?^x5Ov#XRhw9{zr;Q#0*wglhWD={Pn$Qm$;z?Vx)_f>igNB!id zmTlMmkp@8kP212#@jq=m%g4ZEl$*a_T;5nHrbt-6D0@eqFP7u+P`;X_Qk68bzwA0h zf{EW5xAV5fD)il-cV&zFmPG|KV4^Z{YJe-g^>uL2l7Ep|NeA2#;k$yerpffdlXY<2 znDODl8(v(24^8Cs3wr(UajK*lY*9yAqcS>92eF=W8<&GtU-}>|S$M5}kyxz~p>-~Pb{(irc?QF~icx8A201&Xin%Hxx@kekd zw>yHjlemC*8(JFz05gs6x7#7EM|xoGtpVVs0szqB0bqwaqAdVG7&rLc6#(=y0YEA! z=jFw}xeKVfmAMI*+}bv7qH=LK2#X5^06wul0s+}M(f|O@&WMyG9frlGyLb z&Eix=47rL84J+tEWcy_XTyc*xw9uOQy`qmHCjAeJ?d=dUhm;P}^F=LH42AEMIh6X8 z*I7Q1jK%gVlL|8w?%##)xSIY`Y+9$SC8!X*_A*S0SWOKNUtza(FZHahoC2|6f=*oD zxJ8-RZk!+YpG+J}Uqnq$y%y>O^@e5M3SSw^29PMwt%8lX^9FT=O@VX$FCLBdlj#<{ zJWWH<#iU!^E7axvK+`u;$*sGq1SmGYc&{g03Md&$r@btQSUIjl&yJXA&=79FdJ+D< z4K^ORdM{M0b2{wRROvjz1@Rb>5dFb@gfkYiIOAKM(NR3*1JpeR_Hk3>WGvU&>}D^HXZ02JUnM z@1s_HhX#rG7;|FkSh2#agJ_2fREo)L`ws+6{?IeWV(>Dy8A(6)IjpSH-n_uO=810y z#4?ez9NnERv6k)N13sXmx)=sv=$$i_QK`hp%I2cyi*J=ihBWZLwpx9Z#|s;+XI!0s zLjYRVt!1KO;mnb7ZL~XoefWU02f{jcY`2wZ4QK+q7gc4iz%d0)5$tPUg~$jVI6vFO zK^wG7t=**T40km@TNUK+WTx<1mL|6Tn6+kB+E$Gpt8SauF9E-CR9Uui_EHn_nmBqS z>o#G}58nHFtICqJPx<_?UZ;z0_(0&UqMnTftMKW@%AxYpa!g0fxGe060^xkRtYguj ze&fPtC!?RgE}FsE0*^2lnE>42K#jp^nJDyzp{JV*jU?{+%KzW37-q|d3i&%eooE6C8Z2t2 z9bBL;^fzVhdLxCQh1+Ms5P)ilz9MYFKdqYN%*u^ch(Fq~QJASr5V_=szAKA4Xm5M} z(Kka%r!noMtz6ZUbjBrJ?Hy&c+mHB{OFQ}=41Irej{0N90`E*~_F1&7Du+zF{Dky) z+KN|-mmIT`Thcij!{3=ibyIn830G zN{kI3d`NgUEJ|2If}J!?@w~FV+v?~tlo8ps3Nl`3^kI)WfZ0|ms6U8HEvD9HIDWkz6`T_QSewYZyzkRh)!g~R>!jaR9;K|#82kfE5^;R!~}H4C?q{1AG?O$5kGp)G$f%VML%aPD?{ zG6)*KodSZRXbl8OD=ETxQLJz)KMI7xjArKUNh3@0f|T|75?Yy=pD7056ja0W)O;Td zCEJ=7q?d|$3rZb+8Cvt6mybV-#1B2}Jai^DOjM2<90tpql|M5tmheg){2NyZR}x3w zL6u}F+C-PIzZ56q0x$;mVJXM1V0;F}y9F29ob51f;;+)t&7l30gloMMHPTuod530FC}j^4#qOJV%5!&e!H9#!N&XQvs5{R zD_FOomd-uk@?_JiWP%&nQ_myBlM6so1Ffa1aaL7B`!ZTXPg_S%TUS*>M^8iJRj1*~ e{{%>Z1YfTk|3C04d;8A^0$7;Zm{b|L#{L(;l>}-4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f0e7b91d006d22352c9ff2f134e504e3c1d GIT binary patch literal 4842 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..aee44e138434630332d88b1680f33c4b24c70ab3 GIT binary patch literal 10486 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 + + 64dp + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..47c8224 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3890ffd --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + MyGreenDao + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5885930 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/test/java/com/mygreendao/ExampleUnitTest.java b/app/src/test/java/com/mygreendao/ExampleUnitTest.java new file mode 100644 index 0000000..323335d --- /dev/null +++ b/app/src/test/java/com/mygreendao/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.mygreendao; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..74b2ab0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,23 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.2.3' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..04e285f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 28 10:00:20 PST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/greenDao/.gitignore b/greenDao/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/greenDao/.gitignore @@ -0,0 +1 @@ +/build diff --git a/greenDao/build.gradle b/greenDao/build.gradle new file mode 100644 index 0000000..18a988e --- /dev/null +++ b/greenDao/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + + compile 'com.android.support:appcompat-v7:25.1.0' + testCompile 'junit:junit:4.12' + compile 'io.reactivex.rxjava2:rxjava:2.0.2' + compile 'io.reactivex.rxjava2:rxandroid:2.0.1' + compile files('libs/sqlcipher.jar') +} diff --git a/greenDao/libs/sqlcipher.jar b/greenDao/libs/sqlcipher.jar new file mode 100644 index 0000000000000000000000000000000000000000..b3bbb9ed88e4916c2862e0a59f42b931a7a875ae GIT binary patch literal 99763 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%0Dj(XTyp$!ezIwF>f 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
  • Q`VruYoY1RGy&6ujlX*loow(^*?%#IrMdSvJ@aj^WqMc1PcuhUjXA0(1U^l2b+%8 zai*OzNf1x#)iq^nknSXj?Y{`Dmx}`ehcc3}jUw&{s!HJxzBDJl*cvQo%s9yx1AhfC zok@%USV!dBO7J7WW*IrOcw&WT)|mZ%hpCTXcmxS`!PC)9~#kR zZ?%_@Ke@5 zVs{ufR1X*QvqzM=vDJ=i@!48X!YxMYkmEFaFu#S7{e=5P3#`j`bF8oxF@>U}iZfYH zE=D}isV-oBp(MrL%gcUa_1ZklzGIO6BJxr&8xx z3!Oez{CUPq)-7j0#uX=-SMmur&A?ixW?;o<5$IbB$IP<06lzTjYg;VYTjrPfgz>@c z^n(4at{2Cno@_N-`(u#)Prb-o&~~IueX_>+C0|r3Lk=E^M9-PE>i5bz&+^ zHba%NA)OSYWNnbxHjWdU{E3v2N5e&%3uBYbqO+%#eVN>(q9t$^%z(+zA#rB@cl=PR zbz(-ugh0y91kh|8wz=kcw~Cc~Rf;Q^W^LEWGPgPv2Hx=IGO~hc4k44@qYP~sD=s^i zE^X1Ealls3+(2GSS1Zyru)vu3mJU&>saUprX3`-W1XtFkbJFdv!_q3Ms6oxF%4evN zY|*FvWP!=DZU&UL$Z6YK;WWpTt`yob0j4)ZOIN5ISE_C{sX$ckW&|H@d{a}~lgn|f z3gKrt4xu4#7JqVKJ#F0@Hh!3<^4@!JhW2gosK#-Hu<}@&9L1R$v~9$u>|2;*d{QcoA|F*(^G(KV(}=Dsle@i{X1rOtc2(1p&tl#y zWbh_ZkTDau>rU+2pKw=dh5n^|M0Q^`w06w|i@2K_cgNGktF+s%OJzl(4cA+g&@i=X z_Se~O55IJ5XaHk%lr{d#CZ^h1kh86Fn}p?Ap@m<3o9JLJS$Fp_Dd%or-K3Rek@9UJ z&epNAE`>jxjkk;cfJ`;17S;W+6EVhVK$4}sypkeTr<*3z47+9%iQgXGG{D7}3>iz^ z82Pniq+(ZLtpcv>ae~I|0z_@4g0W+Zq-dx59d%|{5#DiBZ?`4{!kqqG;Mz+3b?gUH zc^<6uMI%c#=rpTRQTwNHHoTj%_>>cPdMg8tXs(bajX6Fza)fd1?#$b=j5G=$ht65Z z+yL`#vm0!IG4P`KIBftCRh>ms95cNyS3w%pFTy8}8y5%BnRun}0N7@kRjQ_Rdr>0& z$}EmV1Zi_E(g778&YT3N<=mstb&_TGv1pCXYSe78N9LJNKA!z;DotQk{)uxRpZMLi zW(l5Nf$QBnC1l!9VBOwf0W_np($17s?jx+o$RP@l0z-+P1jQuOphvErC9~+(bx|u= zyHtrlm(^4h>(k;LA~l^gen4yhRveB`v?=9&P{fSv2yM2oXq^PnDTQy!E+EX55xl)= z(GUXq+(aI0F!DKe{XV2myCA?o3OfDP{6sA+%+YNK?=Z5cmok0x`WcTd@*F-dmT7@C z6z;{{@@4FB66QBgUKf4;k#HDca$Wo9)yqla#SS(SW)ppWxI(Ql#fo%I1T*a{H{16-f6;VMtBvpR5~OE*_cnhbt_#s<&bG#_8q+o8dkjvzOdIhC-g z`d35;c~V_|uB`tqgs++!h^Bk+i~v}zy_k?JpvsNcu*en)KPeJD%yE2xZi~v~4M`O3 zWA4i4IgR9vyya#yB5Oa5Na=Kc{Jw@p7HP~n#XfHM8ExdK0jSHg1d%Y z&}BZSki;uQp9gs&$TdDE{If>Xmvcmf=Pa%x{gkAs#Mm3z5uW!c1a<1SrNrHN4FuNt_Y(m zpLNdbg5=|`bA6g%%2t{&7QH%IJoF}GLr zHUcISH;-Yv4KSwhE24*bQkp+l5H9BT-#J?jP&jv_x;bnBjU>Jh!3i@$VL^tBI(GuN zw~O}gY|8$F6(x>3zvcmHiEQWh zz>f^UorlfP@AM?EB<@PU$xY(eJ$lL zs}i&8{gryqirfqLrW>da+WS^?WCi!T$oaQlvdek2=qPQD_q{!ko>}t@X?_0gy6qz;Tcrs zhBR6?OHJ5LztSyIYP3QUf!+Z7>C3$?h-w7;d7p)m87@ba5M)*af!i-34UoV!Du=e;Jaz%2A^odzZ|i9F zwU>s^Si3r4A$}RTipZP-WoOh*6dPy~h+{w)|<0=zMnhW@UCe<40pPYZfZt?&st6SO@dmQRlC%A)l*>SZ#^)35wp zyamR#w#?T^8~ugxn*E(%0i-{*ZPR;&^2kF#`~kyrQ3r|d7gWCkI?_8Bw^K;)H^^+U z*TBtQ=;$ZlJf{@Rzavz0KNrMCdEayQmt4+F!TH`R5@ZKx%1{Ep7MCnD%C zpw9f}Vtk=eblZDJ#XqulsBj3Vbf_<07H$ff=csFX%NOyD3;Vx*Xpp4h#_UR$amWrb zf6@3PLK|Ql2raaSi``jJofu({lCngMD#VX2*wB@<^W>)9fsR6r3Q`$)XEL4PZzh&2 zl94CALH%74>l{rT!i6+Iil+2nv2X+ zAr}K&jt;Bt)^lq6DzmKfq&>>jL&ZX@;nP1WsOSvAq3E%etu}knNd8^CGJ}&A!i8hy zk@f4nSUM$q4Iq7EFw%_qW71UQui(} zEtPIDRc}+!^ZS$6RC`}P{#hIwge#`>7A6{G_R{7ow?uB=_}~I>HgTz}e^Z7`4+r&H zw9-I=%Dw!PdDfnoH|Z@Ik3>w_m|wOUpGeGE=O0V7-wfTQdRp6~^(F5$NBh!(yfUI& zsenOHZ6q*AIzNDWy7}!M9FN5GsHLpj#DpWJ9Z^&$#;#zU3uvD0F}{IWnz4XDO)C{uhuJp)`bzOZofJH6rUfk{zaj`o;c4P z%nBHKCY%kCGv@s}8wj7n50Z3m+U3#qqB4K@ZEo2z$MK87z6iA@T0!3Mno|+D_Nbfs z>YV%Pl<&3resGZd{3&QPT8^R?@`RGVS$H0DeHAwfX%)GIKc5818Yovu_#l$ry!Dbe zau}f^#;J(6Kn!&Unl}>enGzZj&FQ;~?sBjM>kx9P9mhR2-Ppf+HTVU!S+&ka2;1kB zJHO9%wt@+Ns>SLk!ZF`VZ)JRt>ZC1Y_6IDQr|f0^ZbAR?tj0d znBGqc2ay-Xh#QR0Ej<1r-H3Rfe8<|DE<=r@?i0sr;~Y_9Ss{>)N12Hssh^3(a#AvJ z0gSS-fUsa57lxvE0k}MMrTALz?>V&jR$=M#TKI&gkQD~~X!C8N)QtX+Xsp?-Q!VP4 zAOkp^KG=?)@G1{(mBRA|u|9xx`}m8sd=oC-A;vg-MI@(g_w(j_zblB#{)o$AEPG)1{)y+|22QF0@M z3a}P6W=0LlJ9x?$Oum`nKo3Nlp?|NiY z&|a>v*krXrWFGaOdLbNMSlX=H0CsBd95G{dV%tpU%)rlz;kq2y$yc4XpcPmIAWkpMxF#A&o-iAjasI2TnTh(SJqD!=>tCM`Mm!0VZ@>&a4r9@u zcq60%@yX_Pzeh$Z?9%Fe-i6#PLOGn62EAE7IG0;jfKJka-qBbjt&62>Nx=ZBEaKYN zN1RFZH=4goh8R}^IQ~;eqdMZagyOg%AQ8kV)Ui`!F1jj_ zAP=5dlMNinG<3T#Ay8?|AY&h|4PTcf)aqe!bE+~~!gQu708G7UmUBoO<>!$Oe28l# ziC1cgTF=rdb2YunZ3*J$ta6RcjEQFLrYU2xv_c8uWol(?tN(nyPh z(%qfnfB5~pUPkZr9^DM{Je+r}z0R()V=d9P<%HdR;yMSb08gV=Qv8NOh|Moh4Y>(I zp?tv`&lOFb!=wbnTgn7VsPyBJ(t^9JA{l07R&;hA=w!PT2m0W;1Y*oTJoV0^Izo@W zrZfh6u97C-!2Rg%Desf)z_Urp#9B|Y<8V@`IzNYX-AZY!Ia`Y47mHWdJrmN&obpQ% zhLbcZZ?(H;k1^`|B2s;0!sE?ijC)S-KC|77M5#9-GP@xOlSSQ*x#4ybk~o#T;_j;Ea(q;H!t zEp2d-C>?D+UhQ1d7RgsD zhUxX>u2+W%0z)anqyC1c`4#N7i0E$+TP*zd-Hjv3KOo<7}iobQ6bR{v|pz?#*I7o z(uf+GvgMl9!`vTWeKnw4%|jELusKP}puw{x;)Vehw(%Df+B00F5zNG_8n;P%7AW+Q zR(QRF9CcnDWQE%J`s1_GZsES~x4%?918qLidHm zsKpHHR_51*=DMf|+mkBzx`YRw$G`p*_WD4#8VNs!KX#Equbk>o%^d!YRu|Ash^}Z#G3CX=EyX}-(9X4h(5W# zg#7ic+sku({+vlg=auWr{+|A!D-;=0doKrfPoC_hdt%k)Ps)88+RXU2bXX#HzZ>f5 z<8h*}HuBOg{2bxs2*ph9NsH|McsNwZ+y3aW8@{aPhDFkD-q*LAmgAi}kKA1<>)m=LKyn8oOIvsW{QT=vU z!^NgWth9IYdH3g)onA|`;jblHTA$BHSqsR&xv~~OpDW6}9=TtUIlQ^uD`s)jE48}c zE2Ta4)a&CSRtDL^B=qB6#VfK6u#`YEg^0jFL)|>3`IKLdibJEvbwX67NKIvWFntJN zfTs(GM-mMWs2aC$kAvw5E+qBQ_~5e@APi_K5C-(upvdANV|_; zB8BHEIYjGFK)_>$JEm9`AkGde%0e-~c|oM05@}8wQID~Pu=?1kV`E?_b>Ix*zC_=k zr#LW3(MK^nD1as%YG=nwy6|D5>Q1lyX7^gdx4N&_8}G4GACXQ%i(u1ES1EbEDs~9m zoRHA!iJ@-SlVIKSwSC%go#bJQOU~Zt4TDE$)Pzm^W9VAGbWN5{7HGJR};w z8LTgLEZD!L-hxHqL@fI3^-H4kW3lZT>Br55Sxz1!rLVit4Ij51xa*&uHk->r`-9CA z&xLjZo=%z&ec(qnYun9Ytw`LDa5xn2h+v9SBluWvqfoZN;P*9QnS*Ahd{6BQ+H1?Z z<^u`^9f77==LuU9GA^8p)uA<%mMsx%lzn6ZtCiLz(}|%y>=gHBtsEv)Ym|1g?$oHn z;0sF1WKU*9pJuSU;Kg%($FLrSNxXm@ER}?A){$cTSv@IhcCjl<)sMNfj&;QdXVfvl zy2`Nke&Yc&5N3#6g03m7*JiV9`N2YGOs(HL#}nN4z1NjGqADUPu<(O?~g z*(a=iYI#X=;E1;+-*zR%jA2E3swy;fi>u`;8|Fq7CTMgKziocriAlCEq(k8)T32*4 z?~J0@n2c^Y=}Xn6BZ;DfwLvuK@Sy8R5>^abM^D~*<$$vC^WpGjg*^9aw$;#QQ6IMw zF(DD5#Z(}#PKs5#1y6+oz1PN>d}QSasmn0+5_Nj(l^lli+4V2GfoT|G`1yGTAGeaG zPa50e_0u)IlD&vqx`_|#Ju}ip=DCRoPLeC^^FG+7i9K^n8|+Eb$owXL!~@MeI(ETh zSQA+13%{*iXh@Qq1=h-~70asGb?8k@w8t|SiAwdXBlzuh{sHohPWgA829aL;~Y~+2E>kF#B^vJIZiBz zCYi3=Iq6%v9%V$;QlEeirXo8jnVCL!zqe$%@766HhCZepdV~H~OH#?1=Ol*OrRTiI z$g{q#AW!cAL)3)Zf)z_heaN;Lgor*IGhA{fS7S^aV~s0{`~~(x+VwD)uShFCAm|W= z(758lFm-se8!-ZfU~EsXA5D@?0mlg=3DV!NOWvl_j&t*!o+Xc8z8lXL>0Id&#@m!h zAk12eJh?*bDJj`Oz~*fA1`jXsT5|#)i0cJ=2o`&QhCHQQ4d2)#O^_q7>(g~AxoDylUz80c&2Iu)n$e}=I1PCxrfefTy7VTw39W5=sM+^PFWbYBRstUvu zZ|hY}Q)aWf8J6f$fp7J`hM*}zlAg=TTj35W1wZ9u4x(b(Q9O5^`xpDSfx*C3wM2HL znddkAg9)d5_dPvNF%fR1!StCi#?4&BZqN4hVNPJ0T!F-Qh7Xsarp!|i$dgx|>xVb` zG@pfO3xl+>j(Zpk)g=rm z()VhV#c9gn&U4egqoJX8AanDP=~|4OW7!ifEsq5e#9dHX`ayTGBSTT^L@<){M}5Q+ zo_!Co0*@#KGj3wH8guT`hauRHETi|TN6-CQ80bQ3RPU#~dNU;-F*L!mS8PcVw5ZO} zUGG18RT7I4Tvgwqh>{N~(|Mm=i=E<9K7K6!!W-urC7K&%r3UeScNW>k3^Y^UuaSNC zb-o-5`pf_wgfdY=m_Dp?fg3TBNLydmVQuvz2L=idu;r96*-L)NmPOHG4KLZaSSZ%N zM)#GuV_=mK%b)|6N08K&g|S%DyCc4F_|Tgpw0`GF-Ug(kDv5*@Td_uVIZ}~YZco-} zr9Kr`jlai<{1_oAL_n2^c+m4tk5iE_?FT8on)^nMToC&a=WvjmV76QK)3S^qY^@1omvLNVurWN z+9NH{tkUH6(}CBl6j&%qnL{HK*BxRP{mW4JxG|)#-HR=QYtOWM$Lp%*zM+=V$b`W)CJDC{*QH2v_$lkyoU9P=B z@_7oV6N`v}__$h3=iGHI?KijU7W6d^&N z4CQobqYr?q#5_tC*Ft1`aF?CNMCRv+3T?f<-SQrZLZeDHXJXNy=F$6l(jP`Ax+=l4 zSsGtvs3Lp$%G<2KucqvZ+9Vo`4o~$SXXTXNygMOlhE7)G@->D3ZdEKjN0EM=B{X#-Xj`Q zG}htv>5!vI6Z&$`b+m*uk-tG|k=7(uw?}Ir>|iiC6NJLgW+3htiiR#UCtkfEe!{K$ z-h&KlOSz4q)j_biG7yCJ5Feqy37?bqi^=pKuGCQJO&^78}3 zI{G(SVFd*y-5DMH*LvUH7&zbLwOA;x!;fhL92Gk0+EaKqMQ}>n=Vercl^~ z_2G!q?V575ex>(ojhzS4q`_B})KZhukS&JDb*Q6Y_4Fd6PY%$cvU8=kh+e553gmQB z;_-90+eP82)r(?GORNx0i<$23I8U|RG-Id?z}r?+7tf11MQ$z?&Zv+ZTIb|160PKU zw;iEuLDX9PRP)W|!}10B7^;z6;cG8o{Zi&%>raC}U76U~2S=vfc0y1|P^qE%#*+~x za*A(QepHi5dt42?bL--cEVHo(X{_ZpM1vMIaUbLu z6e)|oX67i6q2t;asFJZB2Q^!dT4JB$rMmAm2@l0`t^`xh27fq`;`4OI74u4?;W|vQ z?{%)XKS;9QeER&`8M7C2&U=m!3l5!^hN#SuM>i-EEA?k0dD9<-%+d)DcO~drn{`{Q zzqOtT4T$hk*?pz}@tNoCWOg*FcwLPcZq@6tqkh}dIpblT*7$xw-h;fe2f{J$LmdtR zc_vGe>og#_5vvX!%SRZa%Z!l0{S~*&a@{;_?P#(Q#&z5tLw(=)0&M>;rDWqzac^XE zVLh<~_x`ax(x`l%qpGzI9OmtZ# zMX(+qnrwwO@_j5|&)1&TFh#ueiF^AoVrt7u#aSQPcW zV&C^kKBH(aImkpP38o~F7vU3WVCJeNz$w^WY-1U==i|F{~7-&J=KiA z(fy_yuaZuYIwtJJ>m^tYZ&67#+&-}!(1{j%8wZ(B`mY^Na6VDRk__g4%QMK~4;KeW! z#vN!F*<#$YA**=n?I>&VLQzad?&<89N<-WchPpg#p&bDRSZTW7f4PBCz+p>F&mQCq%#%7nUQAL4Kvl64! zvY1F$^z{j-(W=(*(NgUJ@3)4J?+?kd8;UoN_>9iuYm{#94LR*$s$-)g=RxyO_cPDe zmuviPo~+1)Sk2bF--0^^DI9owl?xa%EC5X*^Dib+(08x{hPsY+_Fy|2Rj~6V@2Meb zPrx`LctS{Wkaggkhz7HUvblayAK*NVq2K4K92FFnKotAgtj00E;PllQ>2q&7TAqZZ z7Zg~+%P-DJXOlDg$0P^*K35%ns9Q{`vR_<%Q?Yp^KO;qwL{lS&LHaer^O1}9DJw`D~qs^9xD`&@S(+;;|O|$eaBLmL3qgd~pvz4n?)vPj4e8DuSF^^H2MI1CsADN_Or2(<^xL?Sj zIjy8_8rr;=;c$JBPZp^x5G5n=8q&o^pEAU4ce0gukGit z++lXE)qIjiAb>&q^z1%+@+7;$#^6+b|5CmZX+USjAclCjq(&xXC^g+J@rMr&ZnPU2 zt+v1Hk8L7dLTq=FuNPgd)g^;0`}r>)-<&xaxY4=FaO&h$ zL%^qVl}gU8l}fI-HDDfx;ALgXz&;i`D$3J<|}Ly@;xpNW_Y! zr%4|hewrM%6>_5<_cNCI7FWbucIX-PtH7>yTzzyom34d_;1yarL3bVT6ZmrVbiE>i zMF;G|t^*=i-Tv3I_rIPOi1~^)Pv%KK+OwtzeLx_JKKjWQ!P*IFI$}&(6@q;WLodJp zX^e9;nI_TNsf8SSO`qqJ+fZKFSbF&ljiQ)AQAAawz4S8Yb&pRi zdyHNhZB37wnAGnco_{zzUC8`a$BwDTc)su+bs~3KHrW58^>(G4Ah0FckYv*Nb*0q% zI+9wV?J42y^GcVA%_jPHr&ln@1sR+K=bGq+Tpn4TL zL+RDsAF>?l@Ey!_*sZmAZXeZNS90AdMZTZ1J>}(cpE+h*1$tfGT|^AK*X)Q2Gw>rN zhM%o?X{b3bbWvhejwE!}$27#8s6_@Nd&@g;kf}cZ!Fbd}72T~Msh%jrI_hwCHH%qE zeyc3}D5;3;H9dnD91Lz;6;7B$0>N38;Y$`PoiKxd`q%iC>5{E++*L&;jlo8|%QCdi zg$l;F#Mfk{6jOv$%b8;QL0b6g*U4y{xh=D{$~tm#_1sKY^sU=558`rfg{1IGxhHq{ z-<-6B8Ki@;syV-MtSoQJ$Hiyu9k|fk9Rg=1e7qIKTYEQAmR@bhtolI+@pf5j*v$Zt z6Jy-E&z&i`Q~8z0_-m#%oQ2)R&=8-hNL}?lY)cokwPRN$w2`;~QzCd3-Z`MX#wiG&ml|GYqW@jQ|zQm-7@S@r_;#Ph`)#nHaC& zy?z$@s=e!u57Q)zbE6okmO_FUN;|H`Js}N$B8IDy>l9YTG$d>LrSQ?y=5`}B9iFxh zydwEWYPc-Q_Kr-TwUyvZu7`mP9Fpy}2`J&ApCw0Q`4qI7j>u?(_!w)OoC;+mW&$_-1)4yX@fiNEN__ZnTknuW>NJi}=H?h0q6~^UZ zw}YkM$!9O#d0Z^sG%%%&ula%+B?{d><70Gp7M1ZcdA)}O8(J*oOQx;E{9ghH9rHzl zLSJuTk`|*4lJ;?ga?RMrpmHOxAa&?IV`HW3on{sddb4trpj>1ZGMXqK5Bc(lDH$bf zmmt#k;UQ-b5=_-X+%G`)8X8*wh0oR-<~U7dobAd{lXsfgC&e%F>kw=5P^H~$C*=1) zCrGSUj6Dp~!dQ3IRPFI6`)tW4Y3v2?yh1}yR-yb@-Q`}d^W{F^8 zV)=w^iXlHdLnWul$1>di0+P!#F5uI-DJrGH^e7t=N--nFr2BE(@Jzf-i6d>sgx@pt z>CzFcXXXsx&;Es`!x=$?+V(TD=Z&xV&;9S`9+iYPO!45qS#?2rPJ88vAUiBzrej!W zS-#WXh%W2#X5uvuEDRZM9XtnK#Vqvsmxv8&J%mYk@pD%Nx>5Nf#G-N2c!jTnr6Zgd z>OuWZ(vr+JRwHaN&9@&nV=rUNV1^%xHswZto@S>39cOX!75I;~1@V3%NqR{(Ii$j= z2E?w_!_+R76$ZQ~XMe)yEAAigCSA9Gc}qHM5KX=$PVE+_s@dQMFaH}khNQSKwa0qO zRE7sA16$I>5iVr34(+oX{jr+9>N??1O;k!XWsAl_4Ju_;@#m6b2X&1ide&lOoZ3`y zDx#%mmlq|<@UMB!NHfG1)47E59FY_tpm2R* zhwT|Hc!KTd>T-z&)d$4m1)0VQr6eTxULUHs)ZXE>khG}#X_Va12<2szC}=bN6sm8V zQscVwy5IOP4l0>R%{PgtWG_4UK%JIFLEUZ$JLTd|$`^lBmSz_2SIBb8LG!irZyL1< zZ#+UUZQ*}V?&BS3S*DhWliOETE;?f4h%%X??9v*{k>e_;5L)pdlTviU(&C9?VOxwg z)uRtFcV*Y#Txl}8)j0++6XbPj&IpGbu<6HW>mP>JXpmTkAt9g7i zD_~f7@*Tpr%+s}|_=7Y9rOH!_3RZ!H!9zQ9RP$`LhWVk~c&)N&w-l7*rB*27Eo8g9 z=u`Y!?W;s1j$?P%ytUofNmuA9EfW3vZI7J~-=GeM>c?=ijMDH!Fgo}}mD?W#r zidR3_FW_qcCN1OFP)COGa_R%Xd4>72=HaiY`5MX|k(5GLd~g=0CSRDn+IoES)i6h3 z;psAtIZsVXwoR4yLUBT|noFX$kSR9n7u1AdCe6>0R9Ja69`?TBJR3|S*0HJ?PGpr^ zob-odk-K)}VVKx-F|xShDjkdK!QE1ys$YbYh>$rMQf@TkSKgwWXc4WB4X~jIsY#_1 zZm;PTTWV{O1|r$ca!hn!=}w0#aBsTt7ogqaTrQlcPkj5h+J46YJT^P zCoEhGB~R`#a?tIFnmI)mI%rm{9vC`>bUmH_LQpu9b?r&b=S~P3c#fu)i7W!g$rt1RMi$4!$Mu2L^zzK|py5NvVsOO!m2b}r?> zZbm6Yr1#$sPr^+<+odv(ZOxciop~0|APzcXjA5Ew1+{&i)Y6nADF78dMjvA+V6`H> z)9DhVfHhc7Z8VS}?`LS#znI%$%hHJQuqm#TK^%rUl=1Q;+D3yFH(lakOL3A_7mUnX z!XTegu8nT=`b0W1=7L_=+!V8FDJW;0Ami=c3})gJDS9iP)(jThE)$G8DF><%OZ?W;BjavPB%@yy+&^FjF%m9%bwhB%~D<*LwQ^*(&TrPzPE*^_YR{rW1 zz1X)Ps&5KQ->^>_V$qXD4rcnkh)MJXVLt8)!cy?A_u@pB*K|Qrk!2%xyY8e*T7717 z(_v5_&8=hFz*K^^)UQuh)FKvLNd8MTgTwo9s-;XRmL-rBorn_s9=fO|&X{vYU&Xuu ztBYvTCtVZ|oGyBe2-PPx`OoY{>#7XDA|0i%nuoCFi3ajx4Lc$fc0}hMq0MQ`&1pQ^ zF?QLabpP~9GP;9DOH&<~5aOT}ba}UnSxYwK^uC6+%7Hct{uS`HLME39Ft&6C(ywDo z8+}6yeG?F?nYA(a`xsMTA3P$AjEpRX?Bs;}&+;_fi%TNs)(+u(ooP;*>-G*FqVh1p4@O744Q%ZQa9ut*mwBAqK`W1r z9*q6~5uJ!Q4nl*Agobc=AS}u?n~Dd#qm~2dS0&X{>x;y`sC^V%Eje&b7=Ma38R?;a-!*l)Nt zx_j0=N2q*2?-N8IBG9Po<$?T7Rr-}5ddQb$|E4-73O+Hen-P-_OE@&KMl0Fn1sHep zi9oV_HFQse@X?^wR8^sby%KB-YK1dFsBDNU90}2{1*ao{&ZBJnVk>)33ByssF z{o)V5d`KK!TO2vP?uEuJ#QUkN&!%Y6AoZ0SZ8SF$)Gk}Vz@{)VKrhEnHQ>}znBXM* zrC|bhGtJDlzlGVU!BSkjJa9&zRz6I)EnKJD2NMcyn}Oc)JS7S64G&o1;ue_3eLgMH zJsgFnm1j0d_mPe&q_i7!*I41*6h@@H)QsX-cZ=hpea&o!ciy$sSNoICh(bN>$vHuD zI%hiRA)FrJLbU7_a!abtGTr{nb+n(fJ?li4ZX&gn+IcJFokjaE@EBN35h5e0YFU*z zmy0jI5etf1pP1ImWFoaJ^IaFLP1#&?2PRjpAmSli9GnF_04jfy{{LV8_XFjxFW3m{ zA{Sp2B-BM%WfUa=w?sc~yMe$1+*Sk$t;J9`!KJ|O7ti|N?f^Xa{q2sA|GVR7>G-#M34h#*a3Kv5 zC|#FJmJv{>4D{_me^cQ5A>-rtN^O2%o`nw3_riPPj|&F5_X|YCz}~?QaGvbPQVkF0pN*?^%9Vj^Phq0;O{mRG19kj z0Nedu*E&sTz$d_rOTYuZjE)CRvtQO#%nodC52$GfvbX<^zT~G8Uh79GIA&J>v=@*7 z#s9isklF{op#Oh{@b@Yv!XDhK1sKeLid=tDF3hF)jta=I&?TId8a(c=#^}fPDJ27QkfDPj z*g?z^Y!5Q}gI(L&X|`+t5ne#LSRwmy!5};8|0wF$5fyw6P*?&2F1qG5AaVR)0-rEQ zuEsxNfiV!^S5?FSZ0GO?trIe^sWt#xX9Eq_#f0{c3kD(7`~&{ND*6VN|IjS-BS;{n z1B->FKux~zr}yK6K@@@B>~dB6%ktm#76V(^m|231)NCz(Ur5>t)|IINdz=F4a_=B$ z@JF`wMQ>qd{U^f==Ui*`0ConHpdlcz|4<(Igh5IS|19HT=fL0n1HwO5mzWV)2%!BD zP%fn|(LK%nME}0Lpm1^e)zl88Zv+%jGh;JDpos=sU-G>OFH{=g`3(O>tp`RQJeP#p zSo}Ak{~r_XyVT!5nXew2rbs}quMV`-mn*?n@P88Vdn|FY^C2f-E?&S~7h5%cTrh}~ z-+#i2+X4O8KXeLT+sM#I0b&LqfbRzx_=G`P{r>>}(NfTS?;s?BQ3&`)oL+eAH|k}) z^8-Yr0eRrRB<3LCzlu?FwKmkS0jdi~=C4;UcsZ%>@{bZ_5D+GE;A`}7(0`Qk`-cF! zrEN(IWB?V&fWV~;R1N<}uo&3R&e7&iRd2{s%OU{~Fae~?Py61;e-tDQ23!0-7Znm- zT5o_{g<4C_s<>rT4xy_Mhm=!27k`zX&i_ zsGg_>-~vE<&2Z@1FKrOlU2fmUG$ok*9$gc-MhB5&95Fh?^%J=XOXkgTC zVyAEQ`?3xncm2oHgh2>^7u0{PL4S+F4&Um+hgSVb4M6=b)Jt0h;NkE=R(`@KNd5qa z3%LSMhL2M4le`ZUn}0RNKgKM8mjNFO;HQk;+y5-%*T?|yTzHq%Ke_Md;JH6K>R(Sp zz*FJws>(*G0npS(iDv*BH*{bYaO{#W+zOEUafke@ht;GO64!~5gO5O`hTcZ~mp zzfgyV|FVA^9tOYF_9rY>^A{N0c&Hb?hQ*xwW3 zHxK moWf63|D>f`|3dqRX=`PTesting documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("org.greenrobot.greendao.test", appContext.getPackageName()); + } +} diff --git a/greenDao/src/main/AndroidManifest.xml b/greenDao/src/main/AndroidManifest.xml new file mode 100644 index 0000000..63b0339 --- /dev/null +++ b/greenDao/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/greenDao/src/main/java/org/greenrobot/greendao/AbstractDao.java b/greenDao/src/main/java/org/greenrobot/greendao/AbstractDao.java new file mode 100644 index 0000000..0747773 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/AbstractDao.java @@ -0,0 +1,1008 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao; + +import android.database.CrossProcessCursor; +import android.database.Cursor; +import android.database.CursorWindow; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; + +import org.greenrobot.greendao.annotation.apihint.Experimental; +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.database.DatabaseStatement; +import org.greenrobot.greendao.identityscope.IdentityScope; +import org.greenrobot.greendao.identityscope.IdentityScopeLong; +import org.greenrobot.greendao.internal.DaoConfig; +import org.greenrobot.greendao.internal.FastCursor; +import org.greenrobot.greendao.internal.TableStatements; +import org.greenrobot.greendao.query.Query; +import org.greenrobot.greendao.query.QueryBuilder; +import org.greenrobot.greendao.rx.RxDao; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import io.reactivex.schedulers.Schedulers; + + +/** + * Base class for all DAOs: Implements entity operations like insert, load, delete, and query. + *

    + * This class is thread-safe. + * + * @param Entity type + * @param Primary key (PK) type; use Void if entity does not have exactly one PK + * @author Markus + */ +/* + * When operating on TX, statements, or identity scope the following locking order must be met to avoid deadlocks: + * + * 1.) If not inside a TX already, begin a TX to acquire a DB connection (connection is to be handled like a lock) + * + * 2.) The DatabaseStatement + * + * 3.) identityScope + */ +public abstract class AbstractDao { + protected final DaoConfig config; + protected final Database db; + protected final boolean isStandardSQLite; + protected final IdentityScope identityScope; + protected final IdentityScopeLong identityScopeLong; + protected final TableStatements statements; + + protected final AbstractDaoSession session; + protected final int pkOrdinal; + + private volatile RxDao rxDao; + private volatile RxDao rxDaoPlain; + + public AbstractDao(DaoConfig config) { + this(config, null); + } + + @SuppressWarnings("unchecked") + public AbstractDao(DaoConfig config, AbstractDaoSession daoSession) { + this.config = config; + this.session = daoSession; + db = config.db; + isStandardSQLite = db.getRawDatabase() instanceof SQLiteDatabase; + identityScope = (IdentityScope) config.getIdentityScope(); + if (identityScope instanceof IdentityScopeLong) { + identityScopeLong = (IdentityScopeLong) identityScope; + } else { + identityScopeLong = null; + } + statements = config.statements; + pkOrdinal = config.pkProperty != null ? config.pkProperty.ordinal : -1; + } + + public AbstractDaoSession getSession() { + return session; + } + + TableStatements getStatements() { + return config.statements; + } + + public String getTablename() { + return config.tablename; + } + + public Property[] getProperties() { + return config.properties; + } + + public Property getPkProperty() { + return config.pkProperty; + } + + public String[] getAllColumns() { + return config.allColumns; + } + + public String[] getPkColumns() { + return config.pkColumns; + } + + public String[] getNonPkColumns() { + return config.nonPkColumns; + } + + /** + * Loads the entity for the given PK. + * + * @param key a PK value or null + * @return The entity or null, if no entity matched the PK value + */ + public T load(K key) { + assertSinglePk(); + if (key == null) { + return null; + } + if (identityScope != null) { + T entity = identityScope.get(key); + if (entity != null) { + return entity; + } + } + String sql = statements.getSelectByKey(); + String[] keyArray = new String[]{key.toString()}; + Cursor cursor = db.rawQuery(sql, keyArray); + return loadUniqueAndCloseCursor(cursor); + } + + public T loadByRowId(long rowId) { + String[] idArray = new String[]{Long.toString(rowId)}; + Cursor cursor = db.rawQuery(statements.getSelectByRowId(), idArray); + return loadUniqueAndCloseCursor(cursor); + } + + protected T loadUniqueAndCloseCursor(Cursor cursor) { + try { + return loadUnique(cursor); + } finally { + cursor.close(); + } + } + + protected T loadUnique(Cursor cursor) { + boolean available = cursor.moveToFirst(); + if (!available) { + return null; + } else if (!cursor.isLast()) { + throw new DaoException("Expected unique result, but count was " + cursor.getCount()); + } + return loadCurrent(cursor, 0, true); + } + + /** Loads all available entities from the database. */ + public List loadAll() { + Cursor cursor = db.rawQuery(statements.getSelectAll(), null); + return loadAllAndCloseCursor(cursor); + } + + /** Detaches an entity from the identity scope (session). Subsequent query results won't return this object. */ + public boolean detach(T entity) { + if (identityScope != null) { + K key = getKeyVerified(entity); + return identityScope.detach(key, entity); + } else { + return false; + } + } + + /** + * Detaches all entities (of type T) from the identity scope (session). Subsequent query results won't return any + * previously loaded objects. + */ + public void detachAll() { + if (identityScope != null) { + identityScope.clear(); + } + } + + protected List loadAllAndCloseCursor(Cursor cursor) { + try { + return loadAllFromCursor(cursor); + } finally { + cursor.close(); + } + } + + /** + * Inserts the given entities in the database using a transaction. + * + * @param entities The entities to insert. + */ + public void insertInTx(Iterable entities) { + insertInTx(entities, isEntityUpdateable()); + } + + /** + * Inserts the given entities in the database using a transaction. + * + * @param entities The entities to insert. + */ + public void insertInTx(T... entities) { + insertInTx(Arrays.asList(entities), isEntityUpdateable()); + } + + /** + * Inserts the given entities in the database using a transaction. The given entities will become tracked if the PK + * is set. + * + * @param entities The entities to insert. + * @param setPrimaryKey if true, the PKs of the given will be set after the insert; pass false to improve + * performance. + */ + public void insertInTx(Iterable entities, boolean setPrimaryKey) { + DatabaseStatement stmt = statements.getInsertStatement(); + executeInsertInTx(stmt, entities, setPrimaryKey); + } + + /** + * Inserts or replaces the given entities in the database using a transaction. The given entities will become + * tracked if the PK is set. + * + * @param entities The entities to insert. + * @param setPrimaryKey if true, the PKs of the given will be set after the insert; pass false to improve + * performance. + */ + public void insertOrReplaceInTx(Iterable entities, boolean setPrimaryKey) { + DatabaseStatement stmt = statements.getInsertOrReplaceStatement(); + executeInsertInTx(stmt, entities, setPrimaryKey); + } + + /** + * Inserts or replaces the given entities in the database using a transaction. + * + * @param entities The entities to insert. + */ + public void insertOrReplaceInTx(Iterable entities) { + insertOrReplaceInTx(entities, isEntityUpdateable()); + } + + /** + * Inserts or replaces the given entities in the database using a transaction. + * + * @param entities The entities to insert. + */ + public void insertOrReplaceInTx(T... entities) { + insertOrReplaceInTx(Arrays.asList(entities), isEntityUpdateable()); + } + + private void executeInsertInTx(DatabaseStatement stmt, Iterable entities, boolean setPrimaryKey) { + db.beginTransaction(); + try { + synchronized (stmt) { + if (identityScope != null) { + identityScope.lock(); + } + try { + if (isStandardSQLite) { + SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement(); + for (T entity : entities) { + bindValues(rawStmt, entity); + if (setPrimaryKey) { + long rowId = rawStmt.executeInsert(); + updateKeyAfterInsertAndAttach(entity, rowId, false); + } else { + rawStmt.execute(); + } + } + } else { + for (T entity : entities) { + bindValues(stmt, entity); + if (setPrimaryKey) { + long rowId = stmt.executeInsert(); + updateKeyAfterInsertAndAttach(entity, rowId, false); + } else { + stmt.execute(); + } + } + } + } finally { + if (identityScope != null) { + identityScope.unlock(); + } + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + /** + * Insert an entity into the table associated with a concrete DAO. + * + * @return row ID of newly inserted entity + */ + public long insert(T entity) { + return executeInsert(entity, statements.getInsertStatement(), true); + } + + /** + * Insert an entity into the table associated with a concrete DAO without setting key property. + *

    + * Warning: This may be faster, but the entity should not be used anymore. The entity also won't be attached to + * identity scope. + * + * @return row ID of newly inserted entity + */ + public long insertWithoutSettingPk(T entity) { + return executeInsert(entity, statements.getInsertOrReplaceStatement(), false); + } + + /** + * Insert an entity into the table associated with a concrete DAO. + * + * @return row ID of newly inserted entity + */ + public long insertOrReplace(T entity) { + return executeInsert(entity, statements.getInsertOrReplaceStatement(), true); + } + + private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) { + long rowId; + if (db.isDbLockedByCurrentThread()) { + rowId = insertInsideTx(entity, stmt); + } else { + // Do TX to acquire a connection before locking the stmt to avoid deadlocks + db.beginTransaction(); + try { + rowId = insertInsideTx(entity, stmt); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + if (setKeyAndAttach) { + updateKeyAfterInsertAndAttach(entity, rowId, true); + } + return rowId; + } + + private long insertInsideTx(T entity, DatabaseStatement stmt) { + synchronized (stmt) { + if (isStandardSQLite) { + SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement(); + bindValues(rawStmt, entity); + return rawStmt.executeInsert(); + } else { + bindValues(stmt, entity); + return stmt.executeInsert(); + } + } + } + + protected void updateKeyAfterInsertAndAttach(T entity, long rowId, boolean lock) { + if (rowId != -1) { + K key = updateKeyAfterInsert(entity, rowId); + attachEntity(key, entity, lock); + } else { + // TODO When does this actually happen? Should we throw instead? + DaoLog.w("Could not insert row (executeInsert returned -1)"); + } + } + + /** + * "Saves" an entity to the database: depending on the existence of the key property, it will be inserted + * (key is null) or updated (key is not null). + *

    + * This is similar to {@link #insertOrReplace(Object)}, but may be more efficient, because if a key is present, + * it does not have to query if that key already exists. + */ + public void save(T entity) { + if (hasKey(entity)) { + update(entity); + } else { + insert(entity); + } + } + + /** + * Saves (see {@link #save(Object)}) the given entities in the database using a transaction. + * + * @param entities The entities to save. + */ + public void saveInTx(T... entities) { + saveInTx(Arrays.asList(entities)); + } + + /** + * Saves (see {@link #save(Object)}) the given entities in the database using a transaction. + * + * @param entities The entities to save. + */ + public void saveInTx(Iterable entities) { + int updateCount = 0; + int insertCount = 0; + for (T entity : entities) { + if (hasKey(entity)) { + updateCount++; + } else { + insertCount++; + } + } + if (updateCount > 0 && insertCount > 0) { + List toUpdate = new ArrayList<>(updateCount); + List toInsert = new ArrayList<>(insertCount); + for (T entity : entities) { + if (hasKey(entity)) { + toUpdate.add(entity); + } else { + toInsert.add(entity); + } + } + + db.beginTransaction(); + try { + updateInTx(toUpdate); + insertInTx(toInsert); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } else if (insertCount > 0) { + insertInTx(entities); + } else if (updateCount > 0) { + updateInTx(entities); + } + } + + /** Reads all available rows from the given cursor and returns a list of entities. */ + protected List loadAllFromCursor(Cursor cursor) { + int count = cursor.getCount(); + if (count == 0) { + return new ArrayList(); + } + List list = new ArrayList(count); + CursorWindow window = null; + boolean useFastCursor = false; + if (cursor instanceof CrossProcessCursor) { + window = ((CrossProcessCursor) cursor).getWindow(); + if (window != null) { // E.g. Robolectric has no Window at this point + if (window.getNumRows() == count) { + cursor = new FastCursor(window); + useFastCursor = true; + } else { + DaoLog.d("Window vs. result size: " + window.getNumRows() + "/" + count); + } + } + } + + if (cursor.moveToFirst()) { + if (identityScope != null) { + identityScope.lock(); + identityScope.reserveRoom(count); + } + + try { + if (!useFastCursor && window != null && identityScope != null) { + loadAllUnlockOnWindowBounds(cursor, window, list); + } else { + do { + list.add(loadCurrent(cursor, 0, false)); + } while (cursor.moveToNext()); + } + } finally { + if (identityScope != null) { + identityScope.unlock(); + } + } + } + return list; + } + + private void loadAllUnlockOnWindowBounds(Cursor cursor, CursorWindow window, List list) { + int windowEnd = window.getStartPosition() + window.getNumRows(); + for (int row = 0; ; row++) { + list.add(loadCurrent(cursor, 0, false)); + row++; + if (row >= windowEnd) { + window = moveToNextUnlocked(cursor); + if (window == null) { + break; + } + windowEnd = window.getStartPosition() + window.getNumRows(); + } else { + if (!cursor.moveToNext()) { + break; + } + } + } + } + + /** + * Unlock identityScope during cursor.moveToNext() when it is about to fill the window (needs a db connection): + * We should not hold the lock while trying to acquire a db connection to avoid deadlocks. + */ + private CursorWindow moveToNextUnlocked(Cursor cursor) { + identityScope.unlock(); + try { + if (cursor.moveToNext()) { + return ((CrossProcessCursor) cursor).getWindow(); + } else { + return null; + } + } finally { + identityScope.lock(); + } + } + + /** Internal use only. Considers identity scope. */ + final protected T loadCurrent(Cursor cursor, int offset, boolean lock) { + if (identityScopeLong != null) { + if (offset != 0) { + // Occurs with deep loads (left outer joins) + if (cursor.isNull(pkOrdinal + offset)) { + return null; + } + } + + long key = cursor.getLong(pkOrdinal + offset); + T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key); + if (entity != null) { + return entity; + } else { + entity = readEntity(cursor, offset); + attachEntity(entity); + if (lock) { + identityScopeLong.put2(key, entity); + } else { + identityScopeLong.put2NoLock(key, entity); + } + return entity; + } + } else if (identityScope != null) { + K key = readKey(cursor, offset); + if (offset != 0 && key == null) { + // Occurs with deep loads (left outer joins) + return null; + } + T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key); + if (entity != null) { + return entity; + } else { + entity = readEntity(cursor, offset); + attachEntity(key, entity, lock); + return entity; + } + } else { + // Check offset, assume a value !=0 indicating a potential outer join, so check PK + if (offset != 0) { + K key = readKey(cursor, offset); + if (key == null) { + // Occurs with deep loads (left outer joins) + return null; + } + } + T entity = readEntity(cursor, offset); + attachEntity(entity); + return entity; + } + } + + /** Internal use only. Considers identity scope. */ + final protected O loadCurrentOther(AbstractDao dao, Cursor cursor, int offset) { + return dao.loadCurrent(cursor, offset, /* TODO check this */true); + } + + /** A raw-style query where you can pass any WHERE clause and arguments. */ + public List queryRaw(String where, String... selectionArg) { + Cursor cursor = db.rawQuery(statements.getSelectAll() + where, selectionArg); + return loadAllAndCloseCursor(cursor); + } + + /** + * Creates a repeatable {@link Query} object based on the given raw SQL where you can pass any WHERE clause and + * arguments. + */ + public Query queryRawCreate(String where, Object... selectionArg) { + List argList = Arrays.asList(selectionArg); + return queryRawCreateListArgs(where, argList); + } + + /** + * Creates a repeatable {@link Query} object based on the given raw SQL where you can pass any WHERE clause and + * arguments. + */ + public Query queryRawCreateListArgs(String where, Collection selectionArg) { + return Query.internalCreate(this, statements.getSelectAll() + where, selectionArg.toArray()); + } + + public void deleteAll() { + // String sql = SqlUtils.createSqlDelete(config.tablename, null); + // db.execSQL(sql); + + db.execSQL("DELETE FROM '" + config.tablename + "'"); + if (identityScope != null) { + identityScope.clear(); + } + } + + /** Deletes the given entity from the database. Currently, only single value PK entities are supported. */ + public void delete(T entity) { + assertSinglePk(); + K key = getKeyVerified(entity); + deleteByKey(key); + } + + /** Deletes an entity with the given PK from the database. Currently, only single value PK entities are supported. */ + public void deleteByKey(K key) { + assertSinglePk(); + DatabaseStatement stmt = statements.getDeleteStatement(); + if (db.isDbLockedByCurrentThread()) { + synchronized (stmt) { + deleteByKeyInsideSynchronized(key, stmt); + } + } else { + // Do TX to acquire a connection before locking the stmt to avoid deadlocks + db.beginTransaction(); + try { + synchronized (stmt) { + deleteByKeyInsideSynchronized(key, stmt); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + if (identityScope != null) { + identityScope.remove(key); + } + } + + private void deleteByKeyInsideSynchronized(K key, DatabaseStatement stmt) { + if (key instanceof Long) { + stmt.bindLong(1, (Long) key); + } else if (key == null) { + throw new DaoException("Cannot delete entity, key is null"); + } else { + stmt.bindString(1, key.toString()); + } + stmt.execute(); + } + + private void deleteInTxInternal(Iterable entities, Iterable keys) { + assertSinglePk(); + DatabaseStatement stmt = statements.getDeleteStatement(); + List keysToRemoveFromIdentityScope = null; + db.beginTransaction(); + try { + synchronized (stmt) { + if (identityScope != null) { + identityScope.lock(); + keysToRemoveFromIdentityScope = new ArrayList(); + } + try { + if (entities != null) { + for (T entity : entities) { + K key = getKeyVerified(entity); + deleteByKeyInsideSynchronized(key, stmt); + if (keysToRemoveFromIdentityScope != null) { + keysToRemoveFromIdentityScope.add(key); + } + } + } + if (keys != null) { + for (K key : keys) { + deleteByKeyInsideSynchronized(key, stmt); + if (keysToRemoveFromIdentityScope != null) { + keysToRemoveFromIdentityScope.add(key); + } + } + } + } finally { + if (identityScope != null) { + identityScope.unlock(); + } + } + } + db.setTransactionSuccessful(); + if (keysToRemoveFromIdentityScope != null && identityScope != null) { + identityScope.remove(keysToRemoveFromIdentityScope); + } + } finally { + db.endTransaction(); + } + } + + /** + * Deletes the given entities in the database using a transaction. + * + * @param entities The entities to delete. + */ + public void deleteInTx(Iterable entities) { + deleteInTxInternal(entities, null); + } + + /** + * Deletes the given entities in the database using a transaction. + * + * @param entities The entities to delete. + */ + public void deleteInTx(T... entities) { + deleteInTxInternal(Arrays.asList(entities), null); + } + + /** + * Deletes all entities with the given keys in the database using a transaction. + * + * @param keys Keys of the entities to delete. + */ + public void deleteByKeyInTx(Iterable keys) { + deleteInTxInternal(null, keys); + } + + /** + * Deletes all entities with the given keys in the database using a transaction. + * + * @param keys Keys of the entities to delete. + */ + public void deleteByKeyInTx(K... keys) { + deleteInTxInternal(null, Arrays.asList(keys)); + } + + /** Resets all locally changed properties of the entity by reloading the values from the database. */ + public void refresh(T entity) { + assertSinglePk(); + K key = getKeyVerified(entity); + String sql = statements.getSelectByKey(); + String[] keyArray = new String[]{key.toString()}; + Cursor cursor = db.rawQuery(sql, keyArray); + try { + boolean available = cursor.moveToFirst(); + if (!available) { + throw new DaoException("Entity does not exist in the database anymore: " + entity.getClass() + + " with key " + key); + } else if (!cursor.isLast()) { + throw new DaoException("Expected unique result, but count was " + cursor.getCount()); + } + readEntity(cursor, entity, 0); + attachEntity(key, entity, true); + } finally { + cursor.close(); + } + } + + public void update(T entity) { + assertSinglePk(); + DatabaseStatement stmt = statements.getUpdateStatement(); + if (db.isDbLockedByCurrentThread()) { + synchronized (stmt) { + if (isStandardSQLite) { + updateInsideSynchronized(entity, (SQLiteStatement) stmt.getRawStatement(), true); + } else { + updateInsideSynchronized(entity, stmt, true); + } + } + } else { + // Do TX to acquire a connection before locking the stmt to avoid deadlocks + db.beginTransaction(); + try { + synchronized (stmt) { + updateInsideSynchronized(entity, stmt, true); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + } + + public QueryBuilder queryBuilder() { + return QueryBuilder.internalCreate(this); + } + + protected void updateInsideSynchronized(T entity, DatabaseStatement stmt, boolean lock) { + // To do? Check if it's worth not to bind PKs here (performance). + bindValues(stmt, entity); + int index = config.allColumns.length + 1; + K key = getKey(entity); + if (key instanceof Long) { + stmt.bindLong(index, (Long) key); + } else if (key == null) { + throw new DaoException("Cannot update entity without key - was it inserted before?"); + } else { + stmt.bindString(index, key.toString()); + } + stmt.execute(); + attachEntity(key, entity, lock); + } + + protected void updateInsideSynchronized(T entity, SQLiteStatement stmt, boolean lock) { + // To do? Check if it's worth not to bind PKs here (performance). + bindValues(stmt, entity); + int index = config.allColumns.length + 1; + K key = getKey(entity); + if (key instanceof Long) { + stmt.bindLong(index, (Long) key); + } else if (key == null) { + throw new DaoException("Cannot update entity without key - was it inserted before?"); + } else { + stmt.bindString(index, key.toString()); + } + stmt.execute(); + attachEntity(key, entity, lock); + } + + /** + * Attaches the entity to the identity scope. Calls attachEntity(T entity). + * + * @param key Needed only for identity scope, pass null if there's none. + * @param entity The entitiy to attach + */ + protected final void attachEntity(K key, T entity, boolean lock) { + attachEntity(entity); + if (identityScope != null && key != null) { + if (lock) { + identityScope.put(key, entity); + } else { + identityScope.putNoLock(key, entity); + } + } + } + + /** + * Sub classes with relations additionally set the DaoMaster here. Must be called before the entity is attached to + * the identity scope. + * + * @param entity The entitiy to attach + */ + protected void attachEntity(T entity) { + } + + /** + * Updates the given entities in the database using a transaction. + * + * @param entities The entities to insert. + */ + public void updateInTx(Iterable entities) { + DatabaseStatement stmt = statements.getUpdateStatement(); + db.beginTransaction(); + RuntimeException txEx = null; + try { + synchronized (stmt) { + if (identityScope != null) { + identityScope.lock(); + } + try { + if (isStandardSQLite) { + SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement(); + for (T entity : entities) { + updateInsideSynchronized(entity, rawStmt, false); + } + } else { + for (T entity : entities) { + updateInsideSynchronized(entity, stmt, false); + } + } + } finally { + if (identityScope != null) { + identityScope.unlock(); + } + } + } + db.setTransactionSuccessful(); + } catch (RuntimeException e) { + txEx = e; + } finally { + try { + db.endTransaction(); + } catch (RuntimeException e) { + if (txEx != null) { + DaoLog.w("Could not end transaction (rethrowing initial exception)", e); + throw txEx; + } else { + throw e; + } + } + } + } + + /** + * Updates the given entities in the database using a transaction. + * + * @param entities The entities to update. + */ + public void updateInTx(T... entities) { + updateInTx(Arrays.asList(entities)); + } + + protected void assertSinglePk() { + if (config.pkColumns.length != 1) { + throw new DaoException(this + " (" + config.tablename + ") does not have a single-column primary key"); + } + } + + public long count() { + return statements.getCountStatement().simpleQueryForLong(); + } + + /** See {@link #getKey(Object)}, but guarantees that the returned key is never null (throws if null). */ + protected K getKeyVerified(T entity) { + K key = getKey(entity); + if (key == null) { + if (entity == null) { + throw new NullPointerException("Entity may not be null"); + } else { + throw new DaoException("Entity has no key"); + } + } else { + return key; + } + } + + /** + * The returned RxDao is a special DAO that let's you interact with Rx Observables without any Scheduler set + * for subscribeOn. + * @see #rx() + */ + @Experimental + public RxDao rxPlain() { + if (rxDaoPlain == null) { + rxDaoPlain = new RxDao<>(this); + } + return rxDaoPlain; + } + + /** + * The returned RxDao is a special DAO that let's you interact with Rx Observables using RX's IO scheduler for + * subscribeOn. + * + * @see #rxPlain() + */ + @Experimental + public RxDao rx() { + if (rxDao == null) { + rxDao = new RxDao<>(this, Schedulers.io()); + } + return rxDao; + } + + /** Gets the SQLiteDatabase for custom database access. Not needed for greenDAO entities. */ + public Database getDatabase() { + return db; + } + + /** Reads the values from the current position of the given cursor and returns a new entity. */ + abstract protected T readEntity(Cursor cursor, int offset); + + + /** Reads the key from the current position of the given cursor, or returns null if there's no single-value key. */ + abstract protected K readKey(Cursor cursor, int offset); + + /** Reads the values from the current position of the given cursor into an existing entity. */ + abstract protected void readEntity(Cursor cursor, T entity, int offset); + + /** Binds the entity's values to the statement. Make sure to synchronize the statement outside of the method. */ + abstract protected void bindValues(DatabaseStatement stmt, T entity); + + /** + * Binds the entity's values to the statement. Make sure to synchronize the enclosing DatabaseStatement outside + * of the method. + */ + protected abstract void bindValues(SQLiteStatement stmt, T entity); + + /** + * Updates the entity's key if possible (only for Long PKs currently). This method must always return the entity's + * key regardless of whether the key existed before or not. + */ + abstract protected K updateKeyAfterInsert(T entity, long rowId); + + /** + * Returns the value of the primary key, if the entity has a single primary key, or, if not, null. Returns null if + * entity is null. + */ + abstract protected K getKey(T entity); + + /** + * Returns true if the entity is not null, and has a non-null key, which is also != 0. + * entity is null. + */ + abstract protected boolean hasKey(T entity); + + /** Returns true if the Entity class can be updated, e.g. for setting the PK after insert. */ + abstract protected boolean isEntityUpdateable(); + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/AbstractDaoMaster.java b/greenDao/src/main/java/org/greenrobot/greendao/AbstractDaoMaster.java new file mode 100644 index 0000000..c9c54e6 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/AbstractDaoMaster.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao; + +import java.util.HashMap; +import java.util.Map; + +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.identityscope.IdentityScopeType; +import org.greenrobot.greendao.internal.DaoConfig; + +/** + * The master of dao will guide you: start dao sessions with the master. + * + * @author Markus + */ +public abstract class AbstractDaoMaster { + protected final Database db; + protected final int schemaVersion; + protected final Map>, DaoConfig> daoConfigMap; + + public AbstractDaoMaster(Database db, int schemaVersion) { + this.db = db; + this.schemaVersion = schemaVersion; + + daoConfigMap = new HashMap>, DaoConfig>(); + } + + protected void registerDaoClass(Class> daoClass) { + DaoConfig daoConfig = new DaoConfig(db, daoClass); + daoConfigMap.put(daoClass, daoConfig); + } + + public int getSchemaVersion() { + return schemaVersion; + } + + /** Gets the SQLiteDatabase for custom database access. Not needed for greenDAO entities. */ + public Database getDatabase() { + return db; + } + + public abstract AbstractDaoSession newSession(); + + public abstract AbstractDaoSession newSession(IdentityScopeType type); +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/AbstractDaoSession.java b/greenDao/src/main/java/org/greenrobot/greendao/AbstractDaoSession.java new file mode 100644 index 0000000..65f8ae6 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/AbstractDaoSession.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +import org.greenrobot.greendao.annotation.apihint.Experimental; +import org.greenrobot.greendao.async.AsyncSession; +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.query.QueryBuilder; +import org.greenrobot.greendao.rx.RxTransaction; + +import io.reactivex.schedulers.Schedulers; + + +/** + * DaoSession gives you access to your DAOs, offers convenient persistence methods, and also serves as a session cache.
    + *
    + * To access the DAOs, call the get{entity}Dao methods by the generated DaoSession sub class.
    + *
    + * DaoSession offers many of the available persistence operations on entities as a convenience. Consider using DAOs + * directly to access all available operations, especially if you call a lot of operations on a single entity type to + * avoid the overhead imposed by DaoSession (the overhead is small, but it may add up).
    + *
    + * By default, the DaoSession has a session cache (IdentityScopeType.Session). The session cache is not just a plain + * data cache to improve performance, but also manages object identities. For example, if you load the same entity twice + * in a query, you will get a single Java object instead of two when using a session cache. This is particular useful + * for relations pointing to a common set of entities. + * + * This class is thread-safe. + * + * @author Markus + * + */ +public class AbstractDaoSession { + private final Database db; + private final Map, AbstractDao> entityToDao; + + private volatile RxTransaction rxTxPlain; + private volatile RxTransaction rxTxIo; + + public AbstractDaoSession(Database db) { + this.db = db; + this.entityToDao = new HashMap, AbstractDao>(); + } + + protected void registerDao(Class entityClass, AbstractDao dao) { + entityToDao.put(entityClass, dao); + } + + /** Convenient call for {@link AbstractDao#insert(Object)}. */ + public long insert(T entity) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entity.getClass()); + return dao.insert(entity); + } + + /** Convenient call for {@link AbstractDao#insertOrReplace(Object)}. */ + public long insertOrReplace(T entity) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entity.getClass()); + return dao.insertOrReplace(entity); + } + + /** Convenient call for {@link AbstractDao#refresh(Object)}. */ + public void refresh(T entity) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entity.getClass()); + dao.refresh(entity); + } + + /** Convenient call for {@link AbstractDao#update(Object)}. */ + public void update(T entity) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entity.getClass()); + dao.update(entity); + } + + /** Convenient call for {@link AbstractDao#delete(Object)}. */ + public void delete(T entity) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entity.getClass()); + dao.delete(entity); + } + + /** Convenient call for {@link AbstractDao#deleteAll()}. */ + public void deleteAll(Class entityClass) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entityClass); + dao.deleteAll(); + } + + /** Convenient call for {@link AbstractDao#load(Object)}. */ + public T load(Class entityClass, K key) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entityClass); + return dao.load(key); + } + + /** Convenient call for {@link AbstractDao#loadAll()}. */ + public List loadAll(Class entityClass) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entityClass); + return dao.loadAll(); + } + + /** Convenient call for {@link AbstractDao#queryRaw(String, String...)}. */ + public List queryRaw(Class entityClass, String where, String... selectionArgs) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entityClass); + return dao.queryRaw(where, selectionArgs); + } + + /** Convenient call for {@link AbstractDao#queryBuilder()}. */ + public QueryBuilder queryBuilder(Class entityClass) { + @SuppressWarnings("unchecked") + AbstractDao dao = (AbstractDao) getDao(entityClass); + return dao.queryBuilder(); + } + + public AbstractDao getDao(Class entityClass) { + AbstractDao dao = entityToDao.get(entityClass); + if (dao == null) { + throw new DaoException("No DAO registered for " + entityClass); + } + return dao; + } + + /** + * Run the given Runnable inside a database transaction. If you except a result, consider callInTx. + */ + public void runInTx(Runnable runnable) { + db.beginTransaction(); + try { + runnable.run(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + /** + * Calls the given Callable inside a database transaction and returns the result of the Callable. If you don't + * except a result, consider runInTx. + */ + public V callInTx(Callable callable) throws Exception { + db.beginTransaction(); + try { + V result = callable.call(); + db.setTransactionSuccessful(); + return result; + } finally { + db.endTransaction(); + } + } + + /** + * Like {@link #callInTx(Callable)} but does not require Exception handling (rethrows an Exception as a runtime + * DaoException). + */ + public V callInTxNoException(Callable callable) { + db.beginTransaction(); + try { + V result; + try { + result = callable.call(); + } catch (Exception e) { + throw new DaoException("Callable failed", e); + } + db.setTransactionSuccessful(); + return result; + } finally { + db.endTransaction(); + } + } + + /** Gets the Database for custom database access. Not needed for greenDAO entities. */ + public Database getDatabase() { + return db; + } + + /** Allows to inspect the meta model using DAOs (e.g. querying table names or properties). */ + public Collection> getAllDaos() { + return Collections.unmodifiableCollection(entityToDao.values()); + } + + /** + * Creates a new {@link AsyncSession} to issue asynchronous entity operations. See {@link AsyncSession} for details. + */ + public AsyncSession startAsyncSession() { + return new AsyncSession(this); + } + + /** + * The returned {@link RxTransaction} allows DB transactions using Rx Observables without any Scheduler set for + * subscribeOn. + * + * @see #rxTx() + */ + @Experimental + public RxTransaction rxTxPlain() { + if (rxTxPlain == null) { + rxTxPlain = new RxTransaction(this); + } + return rxTxPlain; + } + + /** + * The returned {@link RxTransaction} allows DB transactions using Rx Observables using RX's IO scheduler for + * subscribeOn. + * + * @see #rxTxPlain() + */ + @Experimental + public RxTransaction rxTx() { + if (rxTxIo == null) { + rxTxIo = new RxTransaction(this, Schedulers.io()); + } + return rxTxIo; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/DaoException.java b/greenDao/src/main/java/org/greenrobot/greendao/DaoException.java new file mode 100644 index 0000000..d20a0ed --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/DaoException.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao; + +import android.database.SQLException; + +/** + * Exception thrown when something goes wrong in the DAO/ORM layer. + * + * @author Markus + * + */ +public class DaoException extends SQLException { + + private static final long serialVersionUID = -5877937327907457779L; + + public DaoException() { + } + + public DaoException(String error) { + super(error); + } + + public DaoException(String error, Throwable cause) { + super(error); + safeInitCause(cause); + } + + public DaoException(Throwable th) { + safeInitCause(th); + } + + protected void safeInitCause(Throwable cause) { + try { + initCause(cause); + } catch (Throwable e) { + DaoLog.e("Could not set initial cause", e); + DaoLog.e( "Initial cause is:", cause); + } + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/DaoLog.java b/greenDao/src/main/java/org/greenrobot/greendao/DaoLog.java new file mode 100644 index 0000000..edb8e94 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/DaoLog.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao; + +import android.util.Log; + +/** + * Internal greenDAO logger class. A wrapper around the Android Log class providing a static Log Tag. + * + * @author markus + * + */ +public class DaoLog { + private final static String TAG = "greenDAO"; + + public static final int VERBOSE = 2; + public static final int DEBUG = 3; + public static final int INFO = 4; + public static final int WARN = 5; + public static final int ERROR = 6; + public static final int ASSERT = 7; + + public static boolean isLoggable(int level) { + return Log.isLoggable(TAG, level); + } + + public static String getStackTraceString(Throwable th) { + return Log.getStackTraceString(th); + } + + public static int println(int level, String msg) { + return Log.println(level, TAG, msg); + } + + public static int v(String msg) { + return Log.v(TAG, msg); + } + + public static int v(String msg, Throwable th) { + return Log.v(TAG, msg, th); + } + + public static int d(String msg) { + return Log.d(TAG, msg); + } + + public static int d(String msg, Throwable th) { + return Log.d(TAG, msg, th); + } + + public static int i(String msg) { + return Log.i(TAG, msg); + } + + public static int i(String msg, Throwable th) { + return Log.i(TAG, msg, th); + } + + public static int w(String msg) { + return Log.w(TAG, msg); + } + + public static int w(String msg, Throwable th) { + return Log.w(TAG, msg, th); + } + + public static int w(Throwable th) { + return Log.w(TAG, th); + } + + public static int e(String msg) { + return Log.w(TAG, msg); + } + + public static int e(String msg, Throwable th) { + return Log.e(TAG, msg, th); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/DbUtils.java b/greenDao/src/main/java/org/greenrobot/greendao/DbUtils.java new file mode 100644 index 0000000..ca4d49b --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/DbUtils.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; + +import org.greenrobot.greendao.database.Database; + +/** Database utils, for example to execute SQL scripts */ +// TODO add unit tests +public class DbUtils { + + public static void vacuum(Database db) { + db.execSQL("VACUUM"); + } + + /** + * Calls {@link #executeSqlScript(Context, Database, String, boolean)} with transactional set to true. + * + * @return number of statements executed. + */ + public static int executeSqlScript(Context context, Database db, String assetFilename) throws IOException { + return executeSqlScript(context, db, assetFilename, true); + } + + /** + * Executes the given SQL asset in the given database (SQL file should be UTF-8). The database file may contain + * multiple SQL statements. Statements are split using a simple regular expression (something like + * "semicolon before a line break"), not by analyzing the SQL syntax. This will work for many SQL files, but check + * yours. + * + * @return number of statements executed. + */ + public static int executeSqlScript(Context context, Database db, String assetFilename, boolean transactional) + throws IOException { + byte[] bytes = readAsset(context, assetFilename); + String sql = new String(bytes, "UTF-8"); + String[] lines = sql.split(";(\\s)*[\n\r]"); + int count; + if (transactional) { + count = executeSqlStatementsInTx(db, lines); + } else { + count = executeSqlStatements(db, lines); + } + DaoLog.i("Executed " + count + " statements from SQL script '" + assetFilename + "'"); + return count; + } + + public static int executeSqlStatementsInTx(Database db, String[] statements) { + db.beginTransaction(); + try { + int count = executeSqlStatements(db, statements); + db.setTransactionSuccessful(); + return count; + } finally { + db.endTransaction(); + } + } + + public static int executeSqlStatements(Database db, String[] statements) { + int count = 0; + for (String line : statements) { + line = line.trim(); + if (line.length() > 0) { + db.execSQL(line); + count++; + } + } + return count; + } + + /** + * Copies all available data from in to out without closing any stream. + * + * @return number of bytes copied + */ + public static int copyAllBytes(InputStream in, OutputStream out) throws IOException { + int byteCount = 0; + byte[] buffer = new byte[4096]; + while (true) { + int read = in.read(buffer); + if (read == -1) { + break; + } + out.write(buffer, 0, read); + byteCount += read; + } + return byteCount; + } + + public static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + copyAllBytes(in, out); + return out.toByteArray(); + } + + public static byte[] readAsset(Context context, String filename) throws IOException { + InputStream in = context.getResources().getAssets().open(filename); + try { + return readAllBytes(in); + } finally { + in.close(); + } + } + + public static void logTableDump(SQLiteDatabase db, String tablename) { + Cursor cursor = db.query(tablename, null, null, null, null, null, null); + try { + String dump = DatabaseUtils.dumpCursorToString(cursor); + DaoLog.d(dump); + } finally { + cursor.close(); + } + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/InternalQueryDaoAccess.java b/greenDao/src/main/java/org/greenrobot/greendao/InternalQueryDaoAccess.java new file mode 100644 index 0000000..e345349 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/InternalQueryDaoAccess.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao; + +import java.util.List; + +import android.database.Cursor; +import org.greenrobot.greendao.internal.TableStatements; + +/** For internal use by greenDAO only. */ +public final class InternalQueryDaoAccess { + private final AbstractDao dao; + + public InternalQueryDaoAccess(AbstractDao abstractDao) { + dao = abstractDao; + } + + public T loadCurrent(Cursor cursor, int offset, boolean lock) { + return dao.loadCurrent(cursor, offset, lock); + } + + public List loadAllAndCloseCursor(Cursor cursor) { + return dao.loadAllAndCloseCursor(cursor); + } + + public T loadUniqueAndCloseCursor(Cursor cursor) { + return dao.loadUniqueAndCloseCursor(cursor); + } + + public TableStatements getStatements() { + return dao.getStatements(); + } + + public static TableStatements getStatements(AbstractDao dao) { + return dao.getStatements(); + } + +} \ No newline at end of file diff --git a/greenDao/src/main/java/org/greenrobot/greendao/InternalUnitTestDaoAccess.java b/greenDao/src/main/java/org/greenrobot/greendao/InternalUnitTestDaoAccess.java new file mode 100644 index 0000000..248bba6 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/InternalUnitTestDaoAccess.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao; + +import android.database.Cursor; +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.identityscope.IdentityScope; +import org.greenrobot.greendao.internal.DaoConfig; + +import java.lang.reflect.Constructor; + +/** Reserved for internal unit tests that want to access some non-public methods. Don't use for anything else. */ +public class InternalUnitTestDaoAccess { + private final AbstractDao dao; + + public InternalUnitTestDaoAccess(Database db, Class> daoClass, IdentityScope identityScope) + throws Exception { + DaoConfig daoConfig = new DaoConfig(db, daoClass); + daoConfig.setIdentityScope(identityScope); + Constructor> constructor = daoClass.getConstructor(DaoConfig.class); + dao = constructor.newInstance(daoConfig); + } + + public K getKey(T entity) { + return dao.getKey(entity); + } + + public Property[] getProperties() { + return dao.getProperties(); + } + + public boolean isEntityUpdateable() { + return dao.isEntityUpdateable(); + } + + public T readEntity(Cursor cursor, int offset) { + return dao.readEntity(cursor, offset); + } + + public K readKey(Cursor cursor, int offset) { + return dao.readKey(cursor, offset); + } + + public AbstractDao getDao() { + return dao; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/Property.java b/greenDao/src/main/java/org/greenrobot/greendao/Property.java new file mode 100644 index 0000000..b05227d --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/Property.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao; + +import java.util.Collection; + +import org.greenrobot.greendao.internal.SqlUtils; +import org.greenrobot.greendao.query.WhereCondition; +import org.greenrobot.greendao.query.WhereCondition.PropertyCondition; + +/** + * Meta data describing a property mapped to a database column; used to create WhereCondition object used by the query builder. + * + * @author Markus + */ +public class Property { + public final int ordinal; + public final Class type; + public final String name; + public final boolean primaryKey; + public final String columnName; + + public Property(int ordinal, Class type, String name, boolean primaryKey, String columnName) { + this.ordinal = ordinal; + this.type = type; + this.name = name; + this.primaryKey = primaryKey; + this.columnName = columnName; + } + + /** Creates an "equal ('=')" condition for this property. */ + public WhereCondition eq(Object value) { + return new PropertyCondition(this, "=?", value); + } + + /** Creates an "not equal ('<>')" condition for this property. */ + public WhereCondition notEq(Object value) { + return new PropertyCondition(this, "<>?", value); + } + + /** Creates an "LIKE" condition for this property. */ + public WhereCondition like(String value) { + return new PropertyCondition(this, " LIKE ?", value); + } + + /** Creates an "BETWEEN ... AND ..." condition for this property. */ + public WhereCondition between(Object value1, Object value2) { + Object[] values = { value1, value2 }; + return new PropertyCondition(this, " BETWEEN ? AND ?", values); + } + + /** Creates an "IN (..., ..., ...)" condition for this property. */ + public WhereCondition in(Object... inValues) { + StringBuilder condition = new StringBuilder(" IN ("); + SqlUtils.appendPlaceholders(condition, inValues.length).append(')'); + return new PropertyCondition(this, condition.toString(), inValues); + } + + /** Creates an "IN (..., ..., ...)" condition for this property. */ + public WhereCondition in(Collection inValues) { + return in(inValues.toArray()); + } + + /** Creates an "NOT IN (..., ..., ...)" condition for this property. */ + public WhereCondition notIn(Object... notInValues) { + StringBuilder condition = new StringBuilder(" NOT IN ("); + SqlUtils.appendPlaceholders(condition, notInValues.length).append(')'); + return new PropertyCondition(this, condition.toString(), notInValues); + } + + /** Creates an "NOT IN (..., ..., ...)" condition for this property. */ + public WhereCondition notIn(Collection notInValues) { + return notIn(notInValues.toArray()); + } + + /** Creates an "greater than ('>')" condition for this property. */ + public WhereCondition gt(Object value) { + return new PropertyCondition(this, ">?", value); + } + + /** Creates an "less than ('<')" condition for this property. */ + public WhereCondition lt(Object value) { + return new PropertyCondition(this, "=')" condition for this property. */ + public WhereCondition ge(Object value) { + return new PropertyCondition(this, ">=?", value); + } + + /** Creates an "less or equal ('<=')" condition for this property. */ + public WhereCondition le(Object value) { + return new PropertyCondition(this, "<=?", value); + } + + /** Creates an "IS NULL" condition for this property. */ + public WhereCondition isNull() { + return new PropertyCondition(this, " IS NULL"); + } + + /** Creates an "IS NOT NULL" condition for this property. */ + public WhereCondition isNotNull() { + return new PropertyCondition(this, " IS NOT NULL"); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Convert.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Convert.java new file mode 100644 index 0000000..e312b43 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Convert.java @@ -0,0 +1,24 @@ +package org.greenrobot.greendao.annotation; + +import org.greenrobot.greendao.converter.PropertyConverter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies {@link PropertyConverter} for the field to support custom types + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface Convert { + /** Converter class */ + Class converter(); + + /** + * Class of the column which can be persisted in DB. + * This is limited to all java classes which are supported natively by greenDAO. + */ + Class columnType(); +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Entity.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Entity.java new file mode 100644 index 0000000..ef5bf61 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Entity.java @@ -0,0 +1,62 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for entities + * greenDAO only persist objects of classes which are marked with this annotation + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface Entity { + + /** + * Specifies the name on the DB side (e.g. table name) this entity maps to. By default, the name is based on the entities class name. + */ + String nameInDb() default ""; + + /** + * Indexes for the entity. + *

    + * Note: To create a single-column index consider using {@link Index} on the property itself + */ + Index[] indexes() default {}; + + /** + * Advanced flag to disable table creation in the database (when set to false). This can be used to create partial + * entities, which may use only a sub set of properties. Be aware however that greenDAO does not sync multiple + * entities, e.g. in caches. + */ + boolean createInDb() default true; + + /** + * Specifies schema name for the entity: greenDAO can generate independent sets of classes for each schema. + * Entities which belong to different schemas should not have relations. + */ + String schema() default "default"; + + /** + * Whether update/delete/refresh methods should be generated. + * If entity has defined {@link ToMany} or {@link ToOne} relations, then it is active independently from this value + */ + boolean active() default false; + + /** + * Whether an all properties constructor should be generated. A no-args constructor is always required. + */ + boolean generateConstructors() default true; + + /** + * Whether getters and setters for properties should be generated if missing. + */ + boolean generateGettersSetters() default true; + + /** + * Define a protobuf class of this entity to create an additional, special DAO for. + */ + Class protobuf() default void.class; + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Generated.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Generated.java new file mode 100644 index 0000000..183d6ed --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Generated.java @@ -0,0 +1,19 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks that a field, constructor or method was generated by greenDAO + * All the code elements that are marked with this annotation can be changed/removed during next run of generation in + * respect of model changes. + * + * @see Keep + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD}) +public @interface Generated { + int hash() default -1; +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Id.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Id.java new file mode 100644 index 0000000..fbbc2fe --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Id.java @@ -0,0 +1,20 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks field is the primary key of the entity's table + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface Id { + /** + * Specifies that id should be auto-incremented (works only for Long/long fields) + * Autoincrement on SQLite introduces additional resources usage and usually can be avoided + * @see SQLite documentation + */ + boolean autoincrement() default false; +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Index.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Index.java new file mode 100644 index 0000000..ce4c768 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Index.java @@ -0,0 +1,35 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Can be used to: + * - specifies that the property should be indexed + * - define multi-column index through {@link Entity#indexes()} + * + * @see Entity#indexes() + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface Index { + /** + * Comma-separated list of properties that should be indexed, e.g. "propertyA, propertyB, propertyC" + * To specify order, add ASC or DESC after column name, e.g.: "propertyA DESC, propertyB ASC" + * This should be only set if this annotation is used in {@link Entity#indexes()} + */ + String value() default ""; + + /** + * Optional name of the index. + * If omitted, then generated automatically by greenDAO with base on property/properties column name(s) + */ + String name() default ""; + + /** + * Whether the unique constraint should be created with base on this index + */ + boolean unique() default false; +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/JoinEntity.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/JoinEntity.java new file mode 100644 index 0000000..2f4bf9e --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/JoinEntity.java @@ -0,0 +1,22 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines *-to-* relation with join table + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface JoinEntity { + /** Reference to join-entity class, which holds the source and the target properties */ + Class entity(); + + /** Name of the property inside the join entity which holds id of the source (current) entity */ + String sourceProperty(); + + /** Name of the property inside the join entity which holds id of the target entity */ + String targetProperty(); +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/JoinProperty.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/JoinProperty.java new file mode 100644 index 0000000..2f70688 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/JoinProperty.java @@ -0,0 +1,20 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines name and referencedName properties for relations + * + * @see ToMany + */ +@Retention(RetentionPolicy.SOURCE) +@Target({}) +public @interface JoinProperty { + /** Name of the property in the name entity, which matches {@link #referencedName()} */ + String name(); + + /** Name of the property in the referencedName entity, which matches {@link #name()} */ + String referencedName(); +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Keep.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Keep.java new file mode 100644 index 0000000..5afb788 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Keep.java @@ -0,0 +1,24 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies that the target should be kept during next run of greenDAO generation. + *

    + * Using this annotation on an Entity class itself silently disables any class modification. + * The user is responsible to write and support any code which is required for greenDAO. + *

    + *

    + * Don't use this annotation on a class member if you are not completely sure what you are doing, because in + * case of model changes greenDAO will not be able to make corresponding changes into the code of the target. + *

    + * + * @see Generated + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) +public @interface Keep { +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/NotNull.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/NotNull.java new file mode 100644 index 0000000..aa92116 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/NotNull.java @@ -0,0 +1,18 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies that property is not null + *

    + * You can also use any another NotNull or NonNull annotation (from any library or your own), + * they are equal to using this + *

    + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +public @interface NotNull { +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/OrderBy.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/OrderBy.java new file mode 100644 index 0000000..ea981fd --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/OrderBy.java @@ -0,0 +1,23 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies ordering of related collection of {@link ToMany} relation + * E.g.: @OrderBy("name, age DESC") List collection; + * If used as marker (@OrderBy List collection), then collection is ordered by primary key + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface OrderBy { + /** + * Comma-separated list of properties, e.g. "propertyA, propertyB, propertyC" + * To specify direction, add ASC or DESC after property name, e.g.: "propertyA DESC, propertyB ASC" + * Default direction for each property is ASC + * If value is omitted, then collection is ordered by primary key + */ + String value() default ""; +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Property.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Property.java new file mode 100644 index 0000000..79cc988 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Property.java @@ -0,0 +1,19 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Optional: configures the mapped column for a persistent field. + * This annotation is also applicable with @ToOne without additional foreign key property + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface Property { + /** + * Name of the database column for this property. Default is field name. + */ + String nameInDb() default ""; +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/ToMany.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/ToMany.java new file mode 100644 index 0000000..08575f1 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/ToMany.java @@ -0,0 +1,25 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines *-to-N relation + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface ToMany { + /** + * Name of the property inside the target entity which holds id of the source (current) entity + * Required unless no {@link JoinProperty} or {@link JoinEntity} is specified + */ + String referencedJoinProperty() default ""; + + /** + * Array of matching source -> target properties + * Required unless {@link #referencedJoinProperty()} or {@link JoinEntity} is specified + */ + JoinProperty[] joinProperties() default {}; +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/ToOne.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/ToOne.java new file mode 100644 index 0000000..07f9218 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/ToOne.java @@ -0,0 +1,25 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines *-to-1 relation with base on existing property as foreign key or with base on + * automatically created backing column + * + * In case foreignKey is not specified, the following annotations can be applied together with @ToOne: + * - {@link Property} to specify backing column name + * - {@link Unique} to put the unique constraint on backing column during table creation + * - {@link NotNull} to put the NOT NULL constraint on backing column during table creation + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface ToOne { + /** + * Name of the property inside the current entity which holds the key of related entity. + * If this parameter is absent, then an additional column is automatically created to hold the key. + */ + String joinProperty() default ""; +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Transient.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Transient.java new file mode 100644 index 0000000..e8059ef --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Transient.java @@ -0,0 +1,15 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Transient fields are not persisted in the database. + */ +@Retention(SOURCE) +@Target(ElementType.FIELD) +public @interface Transient { +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/Unique.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Unique.java new file mode 100644 index 0000000..7e4eba8 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/Unique.java @@ -0,0 +1,26 @@ +package org.greenrobot.greendao.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks property should have a UNIQUE constraint during table creation. + * This annotation is also applicable with @ToOne without additional foreign key property + * + *

    + * To have a unique constraint after table creation you can use {@link Index#unique()} + *

    + * + *

    + * Note having both @Unique and {@link Index} is redundant and causes performance decrease + * on DB level. See here for more information. + *

    + * + * @see Index#unique() + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface Unique { +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Beta.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Beta.java new file mode 100644 index 0000000..b25a664 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Beta.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.annotation.apihint; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * APIs annotated with @Beta may change and may be even removed in a future release (but is somewhat less likely + * compared to {@link Experimental}). You can still use this API - just be aware that a future version may require + * changes of your code. This is intended for getting feedback on planned features. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) +@Documented +public @interface Beta { +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Experimental.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Experimental.java new file mode 100644 index 0000000..822b251 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Experimental.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.annotation.apihint; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * APIs annotated with @Experimental are likely to change and may be even removed in a future release. + * You can use this API - just be aware that a future version may require changes of your code. + * This is intended for getting feedback on planned features. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) +@Documented +public @interface Experimental { +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Internal.java b/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Internal.java new file mode 100644 index 0000000..9893278 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/annotation/apihint/Internal.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.annotation.apihint; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * APIs annotated with @Internal must NOT be used. + * Internal APIs must only be used by greenDAO and may change or be removed in a future release. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) +@Internal +@Documented +public @interface Internal { +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncDaoException.java b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncDaoException.java new file mode 100644 index 0000000..6d47318 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncDaoException.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.async; + +import org.greenrobot.greendao.DaoException; + +/** + * Used here: {@link AsyncOperation#getResult()}. + * + * @author Markus + */ +public class AsyncDaoException extends DaoException { + + private static final long serialVersionUID = 5872157552005102382L; + + private final AsyncOperation failedOperation; + + public AsyncDaoException(AsyncOperation failedOperation, Throwable cause) { + super(cause); + this.failedOperation = failedOperation; + } + + public AsyncOperation getFailedOperation() { + return failedOperation; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperation.java b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperation.java new file mode 100644 index 0000000..8dd91ae --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperation.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.async; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.database.Database; + +/** + * An operation that will be enqueued for asynchronous execution. + * + * @author Markus + * @see AsyncSession + */ +// TODO Implement Future +public class AsyncOperation { + public enum OperationType { + Insert, InsertInTxIterable, InsertInTxArray, // + InsertOrReplace, InsertOrReplaceInTxIterable, InsertOrReplaceInTxArray, // + Update, UpdateInTxIterable, UpdateInTxArray, // + Delete, DeleteInTxIterable, DeleteInTxArray, // + DeleteByKey, DeleteAll, // + TransactionRunnable, TransactionCallable, // + QueryList, QueryUnique, // + Load, LoadAll, // + Count, Refresh + } + + public static final int FLAG_MERGE_TX = 1; + + /** TODO unused, just an idea */ + public static final int FLAG_STOP_QUEUE_ON_EXCEPTION = 1 << 1; + public static final int FLAG_TRACK_CREATOR_STACKTRACE = 1 << 2; + + final OperationType type; + final AbstractDao dao; + private final Database database; + /** Entity, Iterable, Entity[], or Runnable. */ + final Object parameter; + final int flags; + + volatile long timeStarted; + volatile long timeCompleted; + private volatile boolean completed; + volatile Throwable throwable; + final Exception creatorStacktrace; + volatile Object result; + volatile int mergedOperationsCount; + + int sequenceNumber; + + @SuppressWarnings("unchecked") + /** Either supply dao or database (set other to null). */ + AsyncOperation(OperationType type, AbstractDao dao, Database database, Object parameter, int flags) { + this.type = type; + this.flags = flags; + this.dao = (AbstractDao) dao; + this.database = database; + this.parameter = parameter; + creatorStacktrace = (flags & FLAG_TRACK_CREATOR_STACKTRACE) != 0 ? new Exception("AsyncOperation was created here") : null; + } + + public Throwable getThrowable() { + return throwable; + } + + public void setThrowable(Throwable throwable) { + this.throwable = throwable; + } + + public OperationType getType() { + return type; + } + + public Object getParameter() { + return parameter; + } + + /** + * The operation's result after it has completed. Waits until a result is available. + * + * @return The operation's result or null if the operation type does not produce any result. + * @throws {@link AsyncDaoException} if the operation produced an exception + * @see #waitForCompletion() + */ + public synchronized Object getResult() { + if (!completed) { + waitForCompletion(); + } + if (throwable != null) { + throw new AsyncDaoException(this, throwable); + } + return result; + } + + /** @return true if this operation may be merged with others into a single database transaction. */ + public boolean isMergeTx() { + return (flags & FLAG_MERGE_TX) != 0; + } + + Database getDatabase() { + return database != null ? database : dao.getDatabase(); + } + + /** + * @return true if this operation is mergeable with the given operation. Checks for null, {@link #FLAG_MERGE_TX}, + * and if the database instances match. + */ + boolean isMergeableWith(AsyncOperation other) { + return other != null && isMergeTx() && other.isMergeTx() && getDatabase() == other.getDatabase(); + } + + public long getTimeStarted() { + return timeStarted; + } + + public long getTimeCompleted() { + return timeCompleted; + } + + public long getDuration() { + if (timeCompleted == 0) { + throw new DaoException("This operation did not yet complete"); + } else { + return timeCompleted - timeStarted; + } + } + + public boolean isFailed() { + return throwable != null; + } + + public boolean isCompleted() { + return completed; + } + + /** + * Waits until the operation is complete. If the thread gets interrupted, any {@link InterruptedException} will be + * rethrown as a {@link DaoException}. + * + * @return Result if any, see {@link #getResult()} + */ + public synchronized Object waitForCompletion() { + while (!completed) { + try { + wait(); + } catch (InterruptedException e) { + throw new DaoException("Interrupted while waiting for operation to complete", e); + } + } + return result; + } + + /** + * Waits until the operation is complete, but at most the given amount of milliseconds.If the thread gets + * interrupted, any {@link InterruptedException} will be rethrown as a {@link DaoException}. + * + * @return true if the operation completed in the given time frame. + */ + public synchronized boolean waitForCompletion(int maxMillis) { + if (!completed) { + try { + wait(maxMillis); + } catch (InterruptedException e) { + throw new DaoException("Interrupted while waiting for operation to complete", e); + } + } + return completed; + } + + /** Called when the operation is done. Notifies any threads waiting for this operation's completion. */ + synchronized void setCompleted() { + completed = true; + notifyAll(); + } + + public boolean isCompletedSucessfully() { + return completed && throwable == null; + } + + /** + * If this operation was successfully merged with other operation into a single TX, this will give the count of + * merged operations. If the operation was not merged, it will be 0. + */ + public int getMergedOperationsCount() { + return mergedOperationsCount; + } + + /** + * Each operation get a unique sequence number when the operation is enqueued. Can be used for efficiently + * identifying/mapping operations. + */ + public int getSequenceNumber() { + return sequenceNumber; + } + + /** Reset to prepare another execution run. */ + void reset() { + timeStarted = 0; + timeCompleted = 0; + completed = false; + throwable = null; + result = null; + mergedOperationsCount = 0; + } + + /** + * The stacktrace is captured using an exception if {@link #FLAG_TRACK_CREATOR_STACKTRACE} was used (null + * otherwise). + */ + public Exception getCreatorStacktrace() { + return creatorStacktrace; + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperationExecutor.java b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperationExecutor.java new file mode 100644 index 0000000..1b8b096 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperationExecutor.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.async; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.DaoLog; +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.query.Query; + +import java.util.ArrayList; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +class AsyncOperationExecutor implements Runnable, Handler.Callback { + + private static ExecutorService executorService = Executors.newCachedThreadPool(); + + private final BlockingQueue queue; + private volatile boolean executorRunning; + private volatile int maxOperationCountToMerge; + private volatile AsyncOperationListener listener; + private volatile AsyncOperationListener listenerMainThread; + private volatile int waitForMergeMillis; + + private int countOperationsEnqueued; + private int countOperationsCompleted; + + private Handler handlerMainThread; + private int lastSequenceNumber; + + AsyncOperationExecutor() { + queue = new LinkedBlockingQueue(); + maxOperationCountToMerge = 50; + waitForMergeMillis = 50; + } + + public void enqueue(AsyncOperation operation) { + synchronized (this) { + operation.sequenceNumber = ++lastSequenceNumber; + queue.add(operation); + countOperationsEnqueued++; + if (!executorRunning) { + executorRunning = true; + executorService.execute(this); + } + } + } + + public int getMaxOperationCountToMerge() { + return maxOperationCountToMerge; + } + + public void setMaxOperationCountToMerge(int maxOperationCountToMerge) { + this.maxOperationCountToMerge = maxOperationCountToMerge; + } + + public int getWaitForMergeMillis() { + return waitForMergeMillis; + } + + public void setWaitForMergeMillis(int waitForMergeMillis) { + this.waitForMergeMillis = waitForMergeMillis; + } + + public AsyncOperationListener getListener() { + return listener; + } + + public void setListener(AsyncOperationListener listener) { + this.listener = listener; + } + + public AsyncOperationListener getListenerMainThread() { + return listenerMainThread; + } + + public void setListenerMainThread(AsyncOperationListener listenerMainThread) { + this.listenerMainThread = listenerMainThread; + } + + public synchronized boolean isCompleted() { + return countOperationsEnqueued == countOperationsCompleted; + } + + /** + * Waits until all enqueued operations are complete. If the thread gets interrupted, any + * {@link InterruptedException} will be rethrown as a {@link DaoException}. + */ + public synchronized void waitForCompletion() { + while (!isCompleted()) { + try { + wait(); + } catch (InterruptedException e) { + throw new DaoException("Interrupted while waiting for all operations to complete", e); + } + } + } + + /** + * Waits until all enqueued operations are complete, but at most the given amount of milliseconds. If the thread + * gets interrupted, any {@link InterruptedException} will be rethrown as a {@link DaoException}. + * + * @return true if operations completed in the given time frame. + */ + public synchronized boolean waitForCompletion(int maxMillis) { + if (!isCompleted()) { + try { + wait(maxMillis); + } catch (InterruptedException e) { + throw new DaoException("Interrupted while waiting for all operations to complete", e); + } + } + return isCompleted(); + } + + @Override + public void run() { + try { + try { + while (true) { + AsyncOperation operation = queue.poll(1, TimeUnit.SECONDS); + if (operation == null) { + synchronized (this) { + // Check again, this time in synchronized to be in sync with enqueue(AsyncOperation) + operation = queue.poll(); + if (operation == null) { + // set flag while still inside synchronized + executorRunning = false; + return; + } + } + } + if (operation.isMergeTx()) { + // Wait some ms for another operation to merge because a TX is expensive + AsyncOperation operation2 = queue.poll(waitForMergeMillis, TimeUnit.MILLISECONDS); + if (operation2 != null) { + if (operation.isMergeableWith(operation2)) { + mergeTxAndExecute(operation, operation2); + } else { + // Cannot merge, execute both + executeOperationAndPostCompleted(operation); + executeOperationAndPostCompleted(operation2); + } + continue; + } + } + executeOperationAndPostCompleted(operation); + } + } catch (InterruptedException e) { + DaoLog.w(Thread.currentThread().getName() + " was interruppted", e); + } + } finally { + executorRunning = false; + } + } + + + /** Also checks for other operations in the queue that can be merged into the transaction. */ + private void mergeTxAndExecute(AsyncOperation operation1, AsyncOperation operation2) { + ArrayList mergedOps = new ArrayList(); + mergedOps.add(operation1); + mergedOps.add(operation2); + + Database db = operation1.getDatabase(); + db.beginTransaction(); + boolean success = false; + try { + for (int i = 0; i < mergedOps.size(); i++) { + AsyncOperation operation = mergedOps.get(i); + executeOperation(operation); + if (operation.isFailed()) { + // Operation may still have changed the DB, roll back everything + break; + } + if (i == mergedOps.size() - 1) { + AsyncOperation peekedOp = queue.peek(); + if (i < maxOperationCountToMerge && operation.isMergeableWith(peekedOp)) { + AsyncOperation removedOp = queue.remove(); + if (removedOp != peekedOp) { + // Paranoia check, should not occur unless threading is broken + throw new DaoException("Internal error: peeked op did not match removed op"); + } + mergedOps.add(removedOp); + } else { + // No more ops in the queue to merge, finish it + db.setTransactionSuccessful(); + success = true; + break; + } + } + } + } finally { + try { + db.endTransaction(); + } catch (RuntimeException e) { + DaoLog.i("Async transaction could not be ended, success so far was: " + success, e); + success = false; + } + } + if (success) { + int mergedCount = mergedOps.size(); + for (AsyncOperation asyncOperation : mergedOps) { + asyncOperation.mergedOperationsCount = mergedCount; + handleOperationCompleted(asyncOperation); + } + } else { + DaoLog.i("Reverted merged transaction because one of the operations failed. Executing operations one by " + + "one instead..."); + for (AsyncOperation asyncOperation : mergedOps) { + asyncOperation.reset(); + executeOperationAndPostCompleted(asyncOperation); + } + } + } + + private void handleOperationCompleted(AsyncOperation operation) { + operation.setCompleted(); + + AsyncOperationListener listenerToCall = listener; + if (listenerToCall != null) { + listenerToCall.onAsyncOperationCompleted(operation); + } + if (listenerMainThread != null) { + if (handlerMainThread == null) { + handlerMainThread = new Handler(Looper.getMainLooper(), this); + } + Message msg = handlerMainThread.obtainMessage(1, operation); + handlerMainThread.sendMessage(msg); + } + synchronized (this) { + countOperationsCompleted++; + if (countOperationsCompleted == countOperationsEnqueued) { + notifyAll(); + } + } + } + + private void executeOperationAndPostCompleted(AsyncOperation operation) { + executeOperation(operation); + handleOperationCompleted(operation); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void executeOperation(AsyncOperation operation) { + operation.timeStarted = System.currentTimeMillis(); + try { + switch (operation.type) { + case Delete: + operation.dao.delete(operation.parameter); + break; + case DeleteInTxIterable: + operation.dao.deleteInTx((Iterable) operation.parameter); + break; + case DeleteInTxArray: + operation.dao.deleteInTx((Object[]) operation.parameter); + break; + case Insert: + operation.dao.insert(operation.parameter); + break; + case InsertInTxIterable: + operation.dao.insertInTx((Iterable) operation.parameter); + break; + case InsertInTxArray: + operation.dao.insertInTx((Object[]) operation.parameter); + break; + case InsertOrReplace: + operation.dao.insertOrReplace(operation.parameter); + break; + case InsertOrReplaceInTxIterable: + operation.dao.insertOrReplaceInTx((Iterable) operation.parameter); + break; + case InsertOrReplaceInTxArray: + operation.dao.insertOrReplaceInTx((Object[]) operation.parameter); + break; + case Update: + operation.dao.update(operation.parameter); + break; + case UpdateInTxIterable: + operation.dao.updateInTx((Iterable) operation.parameter); + break; + case UpdateInTxArray: + operation.dao.updateInTx((Object[]) operation.parameter); + break; + case TransactionRunnable: + executeTransactionRunnable(operation); + break; + case TransactionCallable: + executeTransactionCallable(operation); + break; + case QueryList: + operation.result = ((Query) operation.parameter).forCurrentThread().list(); + break; + case QueryUnique: + operation.result = ((Query) operation.parameter).forCurrentThread().unique(); + break; + case DeleteByKey: + operation.dao.deleteByKey(operation.parameter); + break; + case DeleteAll: + operation.dao.deleteAll(); + break; + case Load: + operation.result = operation.dao.load(operation.parameter); + break; + case LoadAll: + operation.result = operation.dao.loadAll(); + break; + case Count: + operation.result = operation.dao.count(); + break; + case Refresh: + operation.dao.refresh(operation.parameter); + break; + default: + throw new DaoException("Unsupported operation: " + operation.type); + } + } catch (Throwable th) { + operation.throwable = th; + } + operation.timeCompleted = System.currentTimeMillis(); + // Do not set it to completed here because it might be a merged TX + } + + private void executeTransactionRunnable(AsyncOperation operation) { + Database db = operation.getDatabase(); + db.beginTransaction(); + try { + ((Runnable) operation.parameter).run(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + @SuppressWarnings("unchecked") + private void executeTransactionCallable(AsyncOperation operation) throws Exception { + Database db = operation.getDatabase(); + db.beginTransaction(); + try { + operation.result = ((Callable) operation.parameter).call(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + @Override + public boolean handleMessage(Message msg) { + AsyncOperationListener listenerToCall = listenerMainThread; + if (listenerToCall != null) { + listenerToCall.onAsyncOperationCompleted((AsyncOperation) msg.obj); + } + return false; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperationListener.java b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperationListener.java new file mode 100644 index 0000000..c294148 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncOperationListener.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.async; + +/** Listener being called after completion of {@link org.greenrobot.greendao.async.AsyncOperation}. */ +public interface AsyncOperationListener { + /** + * Note, that the operation may not have been successful, check + * {@link AsyncOperation#isFailed()} and/or {@link AsyncOperation#getThrowable()} for error situations. + */ + void onAsyncOperationCompleted(AsyncOperation operation); +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncSession.java b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncSession.java new file mode 100644 index 0000000..9be6b40 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/async/AsyncSession.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.async; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.AbstractDaoSession; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.async.AsyncOperation.OperationType; +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.query.Query; + +import java.util.concurrent.Callable; + +/** + * Asynchronous interface to entity operations. All operations will enqueued a @link {@link AsyncOperation} and return + * immediately (fine to call on the UI/main thread). The queue will be processed in a (single) background thread. The + * processing order is the call order of the operations. It's possible to start multiple AsyncSessions that will + * execute + * concurrently. + * + * @author Markus + * @see AbstractDaoSession#startAsyncSession() + */ +// Facade to AsyncOperationExecutor: prepares operations and delegates work to AsyncOperationExecutor. +public class AsyncSession { + private final AbstractDaoSession daoSession; + private final AsyncOperationExecutor executor; + private int sessionFlags; + + public AsyncSession(AbstractDaoSession daoSession) { + this.daoSession = daoSession; + this.executor = new AsyncOperationExecutor(); + } + + public int getMaxOperationCountToMerge() { + return executor.getMaxOperationCountToMerge(); + } + + public void setMaxOperationCountToMerge(int maxOperationCountToMerge) { + executor.setMaxOperationCountToMerge(maxOperationCountToMerge); + } + + public int getWaitForMergeMillis() { + return executor.getWaitForMergeMillis(); + } + + public void setWaitForMergeMillis(int waitForMergeMillis) { + executor.setWaitForMergeMillis(waitForMergeMillis); + } + + public AsyncOperationListener getListener() { + return executor.getListener(); + } + + public void setListener(AsyncOperationListener listener) { + executor.setListener(listener); + } + + public AsyncOperationListener getListenerMainThread() { + return executor.getListenerMainThread(); + } + + public void setListenerMainThread(AsyncOperationListener listenerMainThread) { + executor.setListenerMainThread(listenerMainThread); + } + + public boolean isCompleted() { + return executor.isCompleted(); + } + + /** + * Waits until all enqueued operations are complete. If the thread gets interrupted, any + * {@link InterruptedException} will be rethrown as a {@link DaoException}. + */ + public void waitForCompletion() { + executor.waitForCompletion(); + } + + /** + * Waits until all enqueued operations are complete, but at most the given amount of milliseconds. If the thread + * gets interrupted, any {@link InterruptedException} will be rethrown as a {@link DaoException}. + * + * @return true if operations completed in the given time frame. + */ + public boolean waitForCompletion(int maxMillis) { + return executor.waitForCompletion(maxMillis); + } + + /** Asynchronous version of {@link AbstractDao#insert(Object)}. */ + public AsyncOperation insert(Object entity) { + return insert(entity, 0); + } + + /** Asynchronous version of {@link AbstractDao#insert(Object)}. */ + public AsyncOperation insert(Object entity, int flags) { + return enqueueEntityOperation(OperationType.Insert, entity, flags); + } + + /** Asynchronous version of {@link AbstractDao#insertInTx(Object...)}. */ + public AsyncOperation insertInTx(Class entityClass, E... entities) { + return insertInTx(entityClass, 0, entities); + } + + /** Asynchronous version of {@link AbstractDao#insertInTx(Object...)}. */ + public AsyncOperation insertInTx(Class entityClass, int flags, E... entities) { + return enqueEntityOperation(OperationType.InsertInTxArray, entityClass, entities, flags); + } + + /** Asynchronous version of {@link AbstractDao#insertInTx(Iterable)}. */ + public AsyncOperation insertInTx(Class entityClass, Iterable entities) { + return insertInTx(entityClass, entities, 0); + } + + /** Asynchronous version of {@link AbstractDao#insertInTx(Iterable)}. */ + public AsyncOperation insertInTx(Class entityClass, Iterable entities, int flags) { + return enqueEntityOperation(OperationType.InsertInTxIterable, entityClass, entities, flags); + } + + /** Asynchronous version of {@link AbstractDao#insertOrReplace(Object)}. */ + public AsyncOperation insertOrReplace(Object entity) { + return insertOrReplace(entity, 0); + } + + /** Asynchronous version of {@link AbstractDao#insertOrReplace(Object)}. */ + public AsyncOperation insertOrReplace(Object entity, int flags) { + return enqueueEntityOperation(OperationType.InsertOrReplace, entity, flags); + } + + /** Asynchronous version of {@link AbstractDao#insertOrReplaceInTx(Object...)}. */ + public AsyncOperation insertOrReplaceInTx(Class entityClass, E... entities) { + return insertOrReplaceInTx(entityClass, 0, entities); + } + + /** Asynchronous version of {@link AbstractDao#insertOrReplaceInTx(Object...)}. */ + public AsyncOperation insertOrReplaceInTx(Class entityClass, int flags, E... entities) { + return enqueEntityOperation(OperationType.InsertOrReplaceInTxArray, entityClass, entities, flags); + } + + /** Asynchronous version of {@link AbstractDao#insertOrReplaceInTx(Iterable)}. */ + public AsyncOperation insertOrReplaceInTx(Class entityClass, Iterable entities) { + return insertOrReplaceInTx(entityClass, entities, 0); + } + + /** Asynchronous version of {@link AbstractDao#insertOrReplaceInTx(Iterable)}. */ + public AsyncOperation insertOrReplaceInTx(Class entityClass, Iterable entities, int flags) { + return enqueEntityOperation(OperationType.InsertOrReplaceInTxIterable, entityClass, entities, flags); + } + + /** Asynchronous version of {@link AbstractDao#update(Object)}. */ + public AsyncOperation update(Object entity) { + return update(entity, 0); + } + + /** Asynchronous version of {@link AbstractDao#update(Object)}. */ + public AsyncOperation update(Object entity, int flags) { + return enqueueEntityOperation(OperationType.Update, entity, flags); + } + + /** Asynchronous version of {@link AbstractDao#updateInTx(Object...)}. */ + public AsyncOperation updateInTx(Class entityClass, E... entities) { + return updateInTx(entityClass, 0, entities); + } + + /** Asynchronous version of {@link AbstractDao#updateInTx(Object...)}. */ + public AsyncOperation updateInTx(Class entityClass, int flags, E... entities) { + return enqueEntityOperation(OperationType.UpdateInTxArray, entityClass, entities, flags); + } + + /** Asynchronous version of {@link AbstractDao#updateInTx(Iterable)}. */ + public AsyncOperation updateInTx(Class entityClass, Iterable entities) { + return updateInTx(entityClass, entities, 0); + } + + /** Asynchronous version of {@link AbstractDao#updateInTx(Iterable)}. */ + public AsyncOperation updateInTx(Class entityClass, Iterable entities, int flags) { + return enqueEntityOperation(OperationType.UpdateInTxIterable, entityClass, entities, flags); + } + + /** Asynchronous version of {@link AbstractDao#delete(Object)}. */ + public AsyncOperation delete(Object entity) { + return delete(entity, 0); + } + + /** Asynchronous version of {@link AbstractDao#delete(Object)}. */ + public AsyncOperation delete(Object entity, int flags) { + return enqueueEntityOperation(OperationType.Delete, entity, flags); + } + + /** Asynchronous version of {@link AbstractDao#deleteByKey(Object)}. */ + public AsyncOperation deleteByKey(Object key) { + return deleteByKey(key, 0); + } + + /** Asynchronous version of {@link AbstractDao#deleteByKey(Object)}. */ + public AsyncOperation deleteByKey(Object key, int flags) { + return enqueueEntityOperation(OperationType.DeleteByKey, key, flags); + } + + /** Asynchronous version of {@link AbstractDao#deleteInTx(Object...)}. */ + public AsyncOperation deleteInTx(Class entityClass, E... entities) { + return deleteInTx(entityClass, 0, entities); + } + + /** Asynchronous version of {@link AbstractDao#deleteInTx(Object...)}. */ + public AsyncOperation deleteInTx(Class entityClass, int flags, E... entities) { + return enqueEntityOperation(OperationType.DeleteInTxArray, entityClass, entities, flags); + } + + /** Asynchronous version of {@link AbstractDao#deleteInTx(Iterable)}. */ + public AsyncOperation deleteInTx(Class entityClass, Iterable entities) { + return deleteInTx(entityClass, entities, 0); + } + + /** Asynchronous version of {@link AbstractDao#deleteInTx(Iterable)}. */ + public AsyncOperation deleteInTx(Class entityClass, Iterable entities, int flags) { + return enqueEntityOperation(OperationType.DeleteInTxIterable, entityClass, entities, flags); + } + + /** Asynchronous version of {@link AbstractDao#deleteAll()}. */ + public AsyncOperation deleteAll(Class entityClass) { + return deleteAll(entityClass, 0); + } + + /** Asynchronous version of {@link AbstractDao#deleteAll()}. */ + public AsyncOperation deleteAll(Class entityClass, int flags) { + return enqueEntityOperation(OperationType.DeleteAll, entityClass, null, flags); + } + + /** Asynchronous version of {@link AbstractDaoSession#runInTx(Runnable)}. */ + public AsyncOperation runInTx(Runnable runnable) { + return runInTx(runnable, 0); + } + + /** Asynchronous version of {@link AbstractDaoSession#runInTx(Runnable)}. */ + public AsyncOperation runInTx(Runnable runnable, int flags) { + return enqueueDatabaseOperation(OperationType.TransactionRunnable, runnable, flags); + } + + /** Asynchronous version of {@link AbstractDaoSession#callInTx(Callable)}. */ + public AsyncOperation callInTx(Callable callable) { + return callInTx(callable, 0); + } + + /** Asynchronous version of {@link AbstractDaoSession#callInTx(Callable)}. */ + public AsyncOperation callInTx(Callable callable, int flags) { + return enqueueDatabaseOperation(OperationType.TransactionCallable, callable, flags); + } + + /** Asynchronous version of {@link Query#list()}. */ + public AsyncOperation queryList(Query query) { + return queryList(query, 0); + } + + /** Asynchronous version of {@link Query#list()}. */ + public AsyncOperation queryList(Query query, int flags) { + return enqueueDatabaseOperation(OperationType.QueryList, query, flags); + } + + /** Asynchronous version of {@link Query#unique()}. */ + public AsyncOperation queryUnique(Query query) { + return queryUnique(query, 0); + } + + /** Asynchronous version of {@link Query#unique()}. */ + public AsyncOperation queryUnique(Query query, int flags) { + return enqueueDatabaseOperation(OperationType.QueryUnique, query, flags); + } + + /** Asynchronous version of {@link AbstractDao#load(Object)}. */ + public AsyncOperation load(Class entityClass, Object key) { + return load(entityClass, key, 0); + } + + /** Asynchronous version of {@link AbstractDao#load(Object)}. */ + public AsyncOperation load(Class entityClass, Object key, int flags) { + return enqueEntityOperation(OperationType.Load, entityClass, key, flags); + } + + /** Asynchronous version of {@link AbstractDao#loadAll()}. */ + public AsyncOperation loadAll(Class entityClass) { + return loadAll(entityClass, 0); + } + + /** Asynchronous version of {@link AbstractDao#loadAll()}. */ + public AsyncOperation loadAll(Class entityClass, int flags) { + return enqueEntityOperation(OperationType.LoadAll, entityClass, null, flags); + } + + /** Asynchronous version of {@link AbstractDao#count()}. */ + public AsyncOperation count(Class entityClass) { + return count(entityClass, 0); + } + + /** Asynchronous version of {@link AbstractDao#count()}. */ + public AsyncOperation count(Class entityClass, int flags) { + return enqueEntityOperation(OperationType.Count, entityClass, null, flags); + } + + /** Asynchronous version of {@link AbstractDao#refresh(Object)}. */ + public AsyncOperation refresh(Object entity) { + return refresh(entity, 0); + } + + /** Asynchronous version of {@link AbstractDao#refresh(Object)}. */ + public AsyncOperation refresh(Object entity, int flags) { + return enqueueEntityOperation(OperationType.Refresh, entity, flags); + } + + private AsyncOperation enqueueDatabaseOperation(OperationType type, Object param, int flags) { + Database database = daoSession.getDatabase(); + AsyncOperation operation = new AsyncOperation(type, null, database, param, flags | sessionFlags); + executor.enqueue(operation); + return operation; + } + + private AsyncOperation enqueueEntityOperation(OperationType type, Object entity, int flags) { + return enqueEntityOperation(type, entity.getClass(), entity, flags); + } + + private AsyncOperation enqueEntityOperation(OperationType type, Class entityClass, Object param, int flags) { + AbstractDao dao = daoSession.getDao(entityClass); + AsyncOperation operation = new AsyncOperation(type, dao, null, param, flags | sessionFlags); + executor.enqueue(operation); + return operation; + } + + /** {@link org.greenrobot.greendao.async.AsyncOperation} flags set for all operations (will be ORed with call flags). */ + public int getSessionFlags() { + return sessionFlags; + } + + /** {@link org.greenrobot.greendao.async.AsyncOperation} flags set for all operations (will be ORed with call flags). */ + public void setSessionFlags(int sessionFlags) { + this.sessionFlags = sessionFlags; + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/converter/PropertyConverter.java b/greenDao/src/main/java/org/greenrobot/greendao/converter/PropertyConverter.java new file mode 100644 index 0000000..2ce321d --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/converter/PropertyConverter.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011-2015 Markus Junginger, greenrobot (http://greenrobot.de) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.converter; + +/** + * To use custom types in your entity, implement this to convert db values to entity values and back. + *

    + * Notes for implementations: + *

      + *
    • Converters are created by the default constructor
    • + *
    • Converters must be implemented thread-safe
    • + *
    + */ +public interface PropertyConverter { + P convertToEntityProperty(D databaseValue); + + D convertToDatabaseValue(P entityProperty); +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/database/Database.java b/greenDao/src/main/java/org/greenrobot/greendao/database/Database.java new file mode 100644 index 0000000..fbc1e8c --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/database/Database.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.database; + +import android.database.Cursor; +import android.database.SQLException; + +/** + * Database abstraction used internally by greenDAO. + */ +public interface Database { + Cursor rawQuery(String sql, String[] selectionArgs); + + void execSQL(String sql) throws SQLException; + + void beginTransaction(); + + void endTransaction(); + + boolean inTransaction(); + + void setTransactionSuccessful(); + + void execSQL(String sql, Object[] bindArgs) throws SQLException; + + DatabaseStatement compileStatement(String sql); + + boolean isDbLockedByCurrentThread(); + + void close(); + + Object getRawDatabase(); +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/database/DatabaseOpenHelper.java b/greenDao/src/main/java/org/greenrobot/greendao/database/DatabaseOpenHelper.java new file mode 100644 index 0000000..b11f26e --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/database/DatabaseOpenHelper.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.database; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.database.sqlite.SQLiteOpenHelper; + +/** + * SQLiteOpenHelper to allow working with greenDAO's {@link Database} abstraction to create and update database schemas. + */ +public abstract class DatabaseOpenHelper extends SQLiteOpenHelper { + + private final Context context; + private final String name; + private final int version; + + private EncryptedHelper encryptedHelper; + private boolean loadSQLCipherNativeLibs = true; + + public DatabaseOpenHelper(Context context, String name, int version) { + this(context, name, null, version); + } + + public DatabaseOpenHelper(Context context, String name, CursorFactory factory, int version) { + super(context, name, factory, version); + this.context = context; + this.name = name; + this.version = version; + } + + /** + * Flag to load SQLCipher native libs (default: true). + */ + public void setLoadSQLCipherNativeLibs(boolean loadSQLCipherNativeLibs) { + this.loadSQLCipherNativeLibs = loadSQLCipherNativeLibs; + } + + /** + * Like {@link #getWritableDatabase()}, but returns a greenDAO abstraction of the database. + * The backing DB is an standard {@link SQLiteDatabase}. + */ + public Database getWritableDb() { + return wrap(getWritableDatabase()); + } + + /** + * Like {@link #getReadableDatabase()}, but returns a greenDAO abstraction of the database. + * The backing DB is an standard {@link SQLiteDatabase}. + */ + public Database getReadableDb() { + return wrap(getReadableDatabase()); + } + + protected Database wrap(SQLiteDatabase sqLiteDatabase) { + return new StandardDatabase(sqLiteDatabase); + } + + /** + * Delegates to {@link #onCreate(Database)}, which uses greenDAO's database abstraction. + */ + @Override + public void onCreate(SQLiteDatabase db) { + onCreate(wrap(db)); + } + + /** + * Override this if you do not want to depend on {@link SQLiteDatabase}. + */ + public void onCreate(Database db) { + // Do nothing by default + } + + /** + * Delegates to {@link #onUpgrade(Database, int, int)}, which uses greenDAO's database abstraction. + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + onUpgrade(wrap(db), oldVersion, newVersion); + } + + /** + * Override this if you do not want to depend on {@link SQLiteDatabase}. + */ + public void onUpgrade(Database db, int oldVersion, int newVersion) { + // Do nothing by default + } + + /** + * Delegates to {@link #onOpen(Database)}, which uses greenDAO's database abstraction. + */ + @Override + public void onOpen(SQLiteDatabase db) { + onOpen(wrap(db)); + } + + /** + * Override this if you do not want to depend on {@link SQLiteDatabase}. + */ + public void onOpen(Database db) { + // Do nothing by default + } + + private EncryptedHelper checkEncryptedHelper() { + if (encryptedHelper == null) { + encryptedHelper = new EncryptedHelper(context, name, version, loadSQLCipherNativeLibs); + } + return encryptedHelper; + } + + /** + * Use this to initialize an encrypted SQLCipher database. + * + * @see #onCreate(Database) + * @see #onUpgrade(Database, int, int) + */ + public Database getEncryptedWritableDb(String password) { + EncryptedHelper encryptedHelper = checkEncryptedHelper(); + return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password)); + } + + /** + * Use this to initialize an encrypted SQLCipher database. + * + * @see #onCreate(Database) + * @see #onUpgrade(Database, int, int) + */ + public Database getEncryptedWritableDb(char[] password) { + EncryptedHelper encryptedHelper = checkEncryptedHelper(); + return encryptedHelper.wrap(encryptedHelper.getWritableDatabase(password)); + } + + /** + * Use this to initialize an encrypted SQLCipher database. + * + * @see #onCreate(Database) + * @see #onUpgrade(Database, int, int) + */ + public Database getEncryptedReadableDb(String password) { + EncryptedHelper encryptedHelper = checkEncryptedHelper(); + return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password)); + } + + /** + * Use this to initialize an encrypted SQLCipher database. + * + * @see #onCreate(Database) + * @see #onUpgrade(Database, int, int) + */ + public Database getEncryptedReadableDb(char[] password) { + EncryptedHelper encryptedHelper = checkEncryptedHelper(); + return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password)); + } + + private class EncryptedHelper extends net.sqlcipher.database.SQLiteOpenHelper { + public EncryptedHelper(Context context, String name, int version, boolean loadLibs) { + super(context, name, null, version); + if (loadLibs) { + net.sqlcipher.database.SQLiteDatabase.loadLibs(context); + } + } + + @Override + public void onCreate(net.sqlcipher.database.SQLiteDatabase db) { + DatabaseOpenHelper.this.onCreate(wrap(db)); + } + + @Override + public void onUpgrade(net.sqlcipher.database.SQLiteDatabase db, int oldVersion, int newVersion) { + DatabaseOpenHelper.this.onUpgrade(wrap(db), oldVersion, newVersion); + } + + @Override + public void onOpen(net.sqlcipher.database.SQLiteDatabase db) { + DatabaseOpenHelper.this.onOpen(wrap(db)); + } + + protected Database wrap(net.sqlcipher.database.SQLiteDatabase sqLiteDatabase) { + return new EncryptedDatabase(sqLiteDatabase); + } + + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/database/DatabaseStatement.java b/greenDao/src/main/java/org/greenrobot/greendao/database/DatabaseStatement.java new file mode 100644 index 0000000..b4b4078 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/database/DatabaseStatement.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.database; + +public interface DatabaseStatement { + void execute(); + + long simpleQueryForLong(); + + void bindNull(int index); + + long executeInsert(); + + void bindString(int index, String value); + + void bindBlob(int index, byte[] value); + + void bindLong(int index, long value); + + void clearBindings(); + + void bindDouble(int index, double value); + + void close(); + + Object getRawStatement(); +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/database/EncryptedDatabase.java b/greenDao/src/main/java/org/greenrobot/greendao/database/EncryptedDatabase.java new file mode 100644 index 0000000..3906cab --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/database/EncryptedDatabase.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.database; + +import android.database.Cursor; +import android.database.SQLException; +import net.sqlcipher.database.SQLiteDatabase; + +public class EncryptedDatabase implements Database { + private final SQLiteDatabase delegate; + + public EncryptedDatabase(SQLiteDatabase delegate) { + this.delegate = delegate; + } + + @Override + public Cursor rawQuery(String sql, String[] selectionArgs) { + return delegate.rawQuery(sql, selectionArgs); + } + + @Override + public void execSQL(String sql) throws SQLException { + delegate.execSQL(sql); + } + + @Override + public void beginTransaction() { + delegate.beginTransaction(); + } + + @Override + public void endTransaction() { + delegate.endTransaction(); + } + + @Override + public boolean inTransaction() { + return delegate.inTransaction(); + } + + @Override + public void setTransactionSuccessful() { + delegate.setTransactionSuccessful(); + } + + @Override + public void execSQL(String sql, Object[] bindArgs) throws SQLException { + delegate.execSQL(sql, bindArgs); + } + + @Override + public DatabaseStatement compileStatement(String sql) { + return new EncryptedDatabaseStatement(delegate.compileStatement(sql)); + } + + @Override + public boolean isDbLockedByCurrentThread() { + return delegate.isDbLockedByCurrentThread(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public Object getRawDatabase() { + return delegate; + } + + public SQLiteDatabase getSQLiteDatabase() { + return delegate; + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/database/EncryptedDatabaseStatement.java b/greenDao/src/main/java/org/greenrobot/greendao/database/EncryptedDatabaseStatement.java new file mode 100644 index 0000000..d22806f --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/database/EncryptedDatabaseStatement.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.database; + + +import net.sqlcipher.database.SQLiteStatement; + +public class EncryptedDatabaseStatement implements DatabaseStatement { + private final SQLiteStatement delegate; + + public EncryptedDatabaseStatement(SQLiteStatement delegate) { + this.delegate = delegate; + } + + @Override + public void execute() { + delegate.execute(); + } + + @Override + public long simpleQueryForLong() { + return delegate.simpleQueryForLong(); + } + + @Override + public void bindNull(int index) { + delegate.bindNull(index); + } + + @Override + public long executeInsert() { + return delegate.executeInsert(); + } + + @Override + public void bindString(int index, String value) { + delegate.bindString(index, value); + } + + @Override + public void bindBlob(int index, byte[] value) { + delegate.bindBlob(index, value); + } + + @Override + public void bindLong(int index, long value) { + delegate.bindLong(index, value); + } + + @Override + public void clearBindings() { + delegate.clearBindings(); + } + + @Override + public void bindDouble(int index, double value) { + delegate.bindDouble(index, value); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public Object getRawStatement() { + return delegate; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/database/StandardDatabase.java b/greenDao/src/main/java/org/greenrobot/greendao/database/StandardDatabase.java new file mode 100644 index 0000000..ff74932 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/database/StandardDatabase.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.database; + +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; + +public class StandardDatabase implements Database { + private final SQLiteDatabase delegate; + + public StandardDatabase(SQLiteDatabase delegate) { + this.delegate = delegate; + } + + @Override + public Cursor rawQuery(String sql, String[] selectionArgs) { + return delegate.rawQuery(sql, selectionArgs); + } + + @Override + public void execSQL(String sql) throws SQLException { + delegate.execSQL(sql); + } + + @Override + public void beginTransaction() { + delegate.beginTransaction(); + } + + @Override + public void endTransaction() { + delegate.endTransaction(); + } + + @Override + public boolean inTransaction() { + return delegate.inTransaction(); + } + + @Override + public void setTransactionSuccessful() { + delegate.setTransactionSuccessful(); + } + + @Override + public void execSQL(String sql, Object[] bindArgs) throws SQLException { + delegate.execSQL(sql, bindArgs); + } + + @Override + public DatabaseStatement compileStatement(String sql) { + return new StandardDatabaseStatement(delegate.compileStatement(sql)); + } + + @Override + public boolean isDbLockedByCurrentThread() { + return delegate.isDbLockedByCurrentThread(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public Object getRawDatabase() { + return delegate; + } + + public SQLiteDatabase getSQLiteDatabase() { + return delegate; + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/database/StandardDatabaseStatement.java b/greenDao/src/main/java/org/greenrobot/greendao/database/StandardDatabaseStatement.java new file mode 100644 index 0000000..d74dcfe --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/database/StandardDatabaseStatement.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.database; + +import android.database.sqlite.SQLiteStatement; + +public class StandardDatabaseStatement implements DatabaseStatement { + private final SQLiteStatement delegate; + + public StandardDatabaseStatement(SQLiteStatement delegate) { + this.delegate = delegate; + } + + @Override + public void execute() { + delegate.execute(); + } + + @Override + public long simpleQueryForLong() { + return delegate.simpleQueryForLong(); + } + + @Override + public void bindNull(int index) { + delegate.bindNull(index); + } + + @Override + public long executeInsert() { + return delegate.executeInsert(); + } + + @Override + public void bindString(int index, String value) { + delegate.bindString(index, value); + } + + @Override + public void bindBlob(int index, byte[] value) { + delegate.bindBlob(index, value); + } + + @Override + public void bindLong(int index, long value) { + delegate.bindLong(index, value); + } + + @Override + public void clearBindings() { + delegate.clearBindings(); + } + + @Override + public void bindDouble(int index, double value) { + delegate.bindDouble(index, value); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public Object getRawStatement() { + return delegate; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScope.java b/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScope.java new file mode 100644 index 0000000..724a5be --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScope.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.identityscope; + +/** + * Common interface for a identity scopes needed internally by greenDAO. Identity scopes let greenDAO re-use Java + * objects. + * + * @author Markus + * + * @param + * Key + * @param + * Entity + */ +public interface IdentityScope { + + T get(K key); + + void put(K key, T entity); + + T getNoLock(K key); + + void putNoLock(K key, T entity); + + boolean detach(K key, T entity); + + void remove(K key); + + void remove(Iterable key); + + void clear(); + + void lock(); + + void unlock(); + + void reserveRoom(int count); + +} \ No newline at end of file diff --git a/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeLong.java b/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeLong.java new file mode 100644 index 0000000..77ce470 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeLong.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.identityscope; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.concurrent.locks.ReentrantLock; + +import org.greenrobot.greendao.internal.LongHashMap; + +/** + * The context for entity identities. Provides the scope in which entities will be tracked and managed. + * + * @author Markus + * @param + * Entity + */ +public class IdentityScopeLong implements IdentityScope { + private final LongHashMap> map; + private final ReentrantLock lock; + + public IdentityScopeLong() { + map = new LongHashMap>(); + lock = new ReentrantLock(); + } + + @Override + public T get(Long key) { + return get2(key); + } + + @Override + public T getNoLock(Long key) { + return get2NoLock(key); + } + + public T get2(long key) { + lock.lock(); + Reference ref; + try { + ref = map.get(key); + } finally { + lock.unlock(); + } + if (ref != null) { + return ref.get(); + } else { + return null; + } + } + + public T get2NoLock(long key) { + Reference ref = map.get(key); + if (ref != null) { + return ref.get(); + } else { + return null; + } + } + + @Override + public void put(Long key, T entity) { + put2(key, entity); + } + + @Override + public void putNoLock(Long key, T entity) { + put2NoLock(key, entity); + } + + public void put2(long key, T entity) { + lock.lock(); + try { + map.put(key, new WeakReference(entity)); + } finally { + lock.unlock(); + } + } + + public void put2NoLock(long key, T entity) { + map.put(key, new WeakReference(entity)); + } + + @Override + public boolean detach(Long key, T entity) { + lock.lock(); + try { + if (get(key) == entity && entity != null) { + remove(key); + return true; + } else { + return false; + } + } finally { + lock.unlock(); + } + } + + @Override + public void remove(Long key) { + lock.lock(); + try { + map.remove(key); + } finally { + lock.unlock(); + } + } + + @Override + public void remove(Iterable keys) { + lock.lock(); + try { + for (Long key : keys) { + map.remove(key); + } + } finally { + lock.unlock(); + } + } + + @Override + public void clear() { + lock.lock(); + try { + map.clear(); + } finally { + lock.unlock(); + } + } + + @Override + public void lock() { + lock.lock(); + } + + @Override + public void unlock() { + lock.unlock(); + } + + @Override + public void reserveRoom(int count) { + map.reserveRoom(count); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeObject.java b/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeObject.java new file mode 100644 index 0000000..a4ffe6b --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeObject.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.identityscope; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.concurrent.locks.ReentrantLock; + +/** + * The context for entity identities. Provides the scope in which entities will be tracked and managed. + * + * @author Markus + * @param + * @param + */ +public class IdentityScopeObject implements IdentityScope { + private final HashMap> map; + private final ReentrantLock lock; + + public IdentityScopeObject() { + map = new HashMap>(); + lock = new ReentrantLock(); + } + + @Override + public T get(K key) { + Reference ref; + lock.lock(); + try { + ref = map.get(key); + } finally { + lock.unlock(); + } + if (ref != null) { + return ref.get(); + } else { + return null; + } + } + + @Override + public T getNoLock(K key) { + Reference ref = map.get(key); + if (ref != null) { + return ref.get(); + } else { + return null; + } + } + + @Override + public void put(K key, T entity) { + lock.lock(); + try { + map.put(key, new WeakReference(entity)); + } finally { + lock.unlock(); + } + } + + @Override + public void putNoLock(K key, T entity) { + map.put(key, new WeakReference(entity)); + } + + @Override + public boolean detach(K key, T entity) { + lock.lock(); + try { + if (get(key) == entity && entity != null) { + remove(key); + return true; + } else { + return false; + } + } finally { + lock.unlock(); + } + } + + @Override + public void remove(K key) { + lock.lock(); + try { + map.remove(key); + } finally { + lock.unlock(); + } + } + + @Override + public void remove(Iterable< K> keys) { + lock.lock(); + try { + for (K key : keys) { + map.remove(key); + } + } finally { + lock.unlock(); + } + } + + @Override + public void clear() { + lock.lock(); + try { + map.clear(); + } finally { + lock.unlock(); + } + } + + @Override + public void lock() { + lock.lock(); + } + + @Override + public void unlock() { + lock.unlock(); + } + + @Override + public void reserveRoom(int count) { + // HashMap does not allow + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeType.java b/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeType.java new file mode 100644 index 0000000..62694e9 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/identityscope/IdentityScopeType.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.identityscope; + +public enum IdentityScopeType { + Session, None +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/internal/DaoConfig.java b/greenDao/src/main/java/org/greenrobot/greendao/internal/DaoConfig.java new file mode 100644 index 0000000..7b7b686 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/internal/DaoConfig.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.internal; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.Property; +import org.greenrobot.greendao.identityscope.IdentityScope; +import org.greenrobot.greendao.identityscope.IdentityScopeLong; +import org.greenrobot.greendao.identityscope.IdentityScopeObject; +import org.greenrobot.greendao.identityscope.IdentityScopeType; + +/** + * Internal class used by greenDAO. DaoConfig stores essential data for DAOs, and is hold by AbstractDaoMaster. This + * class will retrieve the required information from the DAO classes. + */ +public final class DaoConfig implements Cloneable { + + public final Database db; + public final String tablename; + public final Property[] properties; + + public final String[] allColumns; + public final String[] pkColumns; + public final String[] nonPkColumns; + + /** Single property PK or null if there's no PK or a multi property PK. */ + public final Property pkProperty; + public final boolean keyIsNumeric; + public final TableStatements statements; + + private IdentityScope identityScope; + + public DaoConfig(Database db, Class> daoClass) { + this.db = db; + try { + this.tablename = (String) daoClass.getField("TABLENAME").get(null); + Property[] properties = reflectProperties(daoClass); + this.properties = properties; + + allColumns = new String[properties.length]; + + List pkColumnList = new ArrayList(); + List nonPkColumnList = new ArrayList(); + Property lastPkProperty = null; + for (int i = 0; i < properties.length; i++) { + Property property = properties[i]; + String name = property.columnName; + allColumns[i] = name; + if (property.primaryKey) { + pkColumnList.add(name); + lastPkProperty = property; + } else { + nonPkColumnList.add(name); + } + } + String[] nonPkColumnsArray = new String[nonPkColumnList.size()]; + nonPkColumns = nonPkColumnList.toArray(nonPkColumnsArray); + String[] pkColumnsArray = new String[pkColumnList.size()]; + pkColumns = pkColumnList.toArray(pkColumnsArray); + + pkProperty = pkColumns.length == 1 ? lastPkProperty : null; + statements = new TableStatements(db, tablename, allColumns, pkColumns); + + if (pkProperty != null) { + Class type = pkProperty.type; + keyIsNumeric = type.equals(long.class) || type.equals(Long.class) || type.equals(int.class) + || type.equals(Integer.class) || type.equals(short.class) || type.equals(Short.class) + || type.equals(byte.class) || type.equals(Byte.class); + } else { + keyIsNumeric = false; + } + + } catch (Exception e) { + throw new DaoException("Could not init DAOConfig", e); + } + } + + private static Property[] reflectProperties(Class> daoClass) + throws ClassNotFoundException, IllegalArgumentException, IllegalAccessException { + Class propertiesClass = Class.forName(daoClass.getName() + "$Properties"); + Field[] fields = propertiesClass.getDeclaredFields(); + + ArrayList propertyList = new ArrayList(); + final int modifierMask = Modifier.STATIC | Modifier.PUBLIC; + for (Field field : fields) { + // There might be other fields introduced by some tools, just ignore them (see issue #28) + if ((field.getModifiers() & modifierMask) == modifierMask) { + Object fieldValue = field.get(null); + if (fieldValue instanceof Property) { + propertyList.add((Property) fieldValue); + } + } + } + + Property[] properties = new Property[propertyList.size()]; + for (Property property : propertyList) { + if (properties[property.ordinal] != null) { + throw new DaoException("Duplicate property ordinals"); + } + properties[property.ordinal] = property; + } + return properties; + } + + /** Does not copy identity scope. */ + public DaoConfig(DaoConfig source) { + db = source.db; + tablename = source.tablename; + properties = source.properties; + allColumns = source.allColumns; + pkColumns = source.pkColumns; + nonPkColumns = source.nonPkColumns; + pkProperty = source.pkProperty; + statements = source.statements; + keyIsNumeric = source.keyIsNumeric; + } + + /** Does not copy identity scope. */ + @Override + public DaoConfig clone() { + return new DaoConfig(this); + } + + public IdentityScope getIdentityScope() { + return identityScope; + } + + /** + * Clears the identify scope if it exists. + */ + public void clearIdentityScope() { + IdentityScope identityScope = this.identityScope; + if(identityScope != null) { + identityScope.clear(); + } + } + + public void setIdentityScope(IdentityScope identityScope) { + this.identityScope = identityScope; + } + + @SuppressWarnings("rawtypes") + public void initIdentityScope(IdentityScopeType type) { + if (type == IdentityScopeType.None) { + identityScope = null; + } else if (type == IdentityScopeType.Session) { + if (keyIsNumeric) { + identityScope = new IdentityScopeLong(); + } else { + identityScope = new IdentityScopeObject(); + } + } else { + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/internal/FastCursor.java b/greenDao/src/main/java/org/greenrobot/greendao/internal/FastCursor.java new file mode 100644 index 0000000..f3bb037 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/internal/FastCursor.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.internal; + +import android.content.ContentResolver; +import android.database.CharArrayBuffer; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.CursorWindow; +import android.database.DataSetObserver; +import android.net.Uri; +import android.os.Bundle; + +/** Internal class used by greenDAO. */ +final public class FastCursor implements Cursor { + + private final CursorWindow window; + private int position; + private final int count; + + public FastCursor(CursorWindow window) { + this.window = window; + count = window.getNumRows(); + } + + @Override + public int getCount() { + return window.getNumRows(); + } + + @Override + public int getPosition() { + return position; + } + + @Override + public boolean move(int offset) { + return moveToPosition(position + offset); + } + + @Override + public boolean moveToPosition(int position) { + if (position >= 0 && position < count) { + this.position = position; + return true; + } else { + return false; + } + } + + @Override + public boolean moveToFirst() { + position = 0; + return count > 0; + } + + @Override + public boolean moveToLast() { + if (count > 0) { + position = count - 1; + return true; + } else { + return false; + } + } + + @Override + public boolean moveToNext() { + if (position < count - 1) { + position++; + return true; + } else { + return false; + } + } + + @Override + public boolean moveToPrevious() { + if (position > 0) { + position--; + return true; + } else { + return false; + } + } + + @Override + public boolean isFirst() { + return position == 0; + } + + @Override + public boolean isLast() { + return position == count - 1; + } + + @Override + public boolean isBeforeFirst() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isAfterLast() { + throw new UnsupportedOperationException(); + } + + @Override + public int getColumnIndex(String columnName) { + throw new UnsupportedOperationException(); + } + + @Override + public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public String getColumnName(int columnIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getColumnNames() { + throw new UnsupportedOperationException(); + } + + @Override + public int getColumnCount() { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getBlob(int columnIndex) { + return window.getBlob(position, columnIndex); + } + + @Override + public String getString(int columnIndex) { + return window.getString(position, columnIndex); + } + + @Override + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public short getShort(int columnIndex) { + return window.getShort(position, columnIndex); + } + + @Override + public int getInt(int columnIndex) { + return window.getInt(position, columnIndex); + } + + @Override + public long getLong(int columnIndex) { + return window.getLong(position, columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + return window.getFloat(position, columnIndex); + } + + @Override + public double getDouble(int columnIndex) { + return window.getDouble(position, columnIndex); + } + + @SuppressWarnings("deprecation") + @Override + public boolean isNull(int columnIndex) { + return window.isNull(position, columnIndex); + } + + @Override + public void deactivate() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean requery() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isClosed() { + throw new UnsupportedOperationException(); + } + + @Override + public void registerContentObserver(ContentObserver observer) { + throw new UnsupportedOperationException(); + } + + @Override + public void unregisterContentObserver(ContentObserver observer) { + throw new UnsupportedOperationException(); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + throw new UnsupportedOperationException(); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + throw new UnsupportedOperationException(); + } + + @Override + public void setNotificationUri(ContentResolver cr, Uri uri) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getWantsAllOnMoveCalls() { + throw new UnsupportedOperationException(); + } + + @Override + public void setExtras(Bundle extras) { + + } + + @Override + public Bundle getExtras() { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle respond(Bundle extras) { + throw new UnsupportedOperationException(); + } + + /** Since API level 11 */ + public int getType(int columnIndex) { + throw new UnsupportedOperationException(); + } + + /** Since API level 19 */ + public Uri getNotificationUri() { + return null; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/internal/LongHashMap.java b/greenDao/src/main/java/org/greenrobot/greendao/internal/LongHashMap.java new file mode 100644 index 0000000..c816d54 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/internal/LongHashMap.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.internal; + +import java.util.Arrays; + +import org.greenrobot.greendao.DaoLog; + +/** + * An minimalistic hash map optimized for long keys. + * + * @author Markus + * + * @param + * The class to store. + */ +public final class LongHashMap { + final static class Entry { + final long key; + T value; + Entry next; + + Entry(long key, T value, Entry next) { + this.key = key; + this.value = value; + this.next = next; + } + } + + private Entry[] table; + private int capacity; + private int threshold; + private int size; + + public LongHashMap() { + this(16); + } + + @SuppressWarnings("unchecked") + public LongHashMap(int capacity) { + this.capacity = capacity; + this.threshold = capacity * 4 / 3; + this.table = new Entry[capacity]; + } + + public boolean containsKey(long key) { + final int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % capacity; + + for (Entry entry = table[index]; entry != null; entry = entry.next) { + if (entry.key == key) { + return true; + } + } + return false; + } + + public T get(long key) { + final int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % capacity; + for (Entry entry = table[index]; entry != null; entry = entry.next) { + if (entry.key == key) { + return entry.value; + } + } + return null; + } + + public T put(long key, T value) { + final int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % capacity; + final Entry entryOriginal = table[index]; + for (Entry entry = entryOriginal; entry != null; entry = entry.next) { + if (entry.key == key) { + T oldValue = entry.value; + entry.value = value; + return oldValue; + } + } + table[index] = new Entry(key, value, entryOriginal); + size++; + if (size > threshold) { + setCapacity(2 * capacity); + } + return null; + } + + public T remove(long key) { + int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % capacity; + Entry previous = null; + Entry entry = table[index]; + while (entry != null) { + Entry next = entry.next; + if (entry.key == key) { + if (previous == null) { + table[index] = next; + } else { + previous.next = next; + } + size--; + return entry.value; + } + previous = entry; + entry = next; + } + return null; + } + + public void clear() { + size = 0; + Arrays.fill(table, null); + } + + public int size() { + return size; + } + + public void setCapacity(int newCapacity) { + @SuppressWarnings("unchecked") + Entry[] newTable = new Entry[newCapacity]; + int length = table.length; + for (int i = 0; i < length; i++) { + Entry entry = table[i]; + while (entry != null) { + long key = entry.key; + int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % newCapacity; + + Entry originalNext = entry.next; + entry.next = newTable[index]; + newTable[index] = entry; + entry = originalNext; + } + } + table = newTable; + capacity = newCapacity; + threshold = newCapacity * 4 / 3; + } + + /** Target load: 0,6 */ + public void reserveRoom(int entryCount) { + setCapacity(entryCount * 5 / 3); + } + + public void logStats() { + int collisions = 0; + for (Entry entry : table) { + while (entry != null && entry.next != null) { + collisions++; + entry = entry.next; + } + } + DaoLog.d("load: " + ((float) size) / capacity + ", size: " + size + ", capa: " + capacity + ", collisions: " + + collisions + ", collision ratio: " + ((float) collisions) / size); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/internal/SqlUtils.java b/greenDao/src/main/java/org/greenrobot/greendao/internal/SqlUtils.java new file mode 100644 index 0000000..1ce0add --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/internal/SqlUtils.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.internal; + +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.Property; + +/** Helper class to create SQL statements as used by greenDAO internally. */ +public class SqlUtils { + private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + public static StringBuilder appendProperty(StringBuilder builder, String tablePrefix, Property property) { + if (tablePrefix != null) { + builder.append(tablePrefix).append('.'); + } + builder.append('"').append(property.columnName).append('"'); + return builder; + } + + public static StringBuilder appendColumn(StringBuilder builder, String column) { + builder.append('"').append(column).append('"'); + return builder; + } + + public static StringBuilder appendColumn(StringBuilder builder, String tableAlias, String column) { + builder.append(tableAlias).append(".\"").append(column).append('"'); + return builder; + } + + public static StringBuilder appendColumns(StringBuilder builder, String tableAlias, String[] columns) { + int length = columns.length; + for (int i = 0; i < length; i++) { + appendColumn(builder, tableAlias, columns[i]); + if (i < length - 1) { + builder.append(','); + } + } + return builder; + } + + public static StringBuilder appendColumns(StringBuilder builder, String[] columns) { + int length = columns.length; + for (int i = 0; i < length; i++) { + builder.append('"').append(columns[i]).append('"'); + if (i < length - 1) { + builder.append(','); + } + } + return builder; + } + + public static StringBuilder appendPlaceholders(StringBuilder builder, int count) { + for (int i = 0; i < count; i++) { + if (i < count - 1) { + builder.append("?,"); + } else { + builder.append('?'); + } + } + return builder; + } + + public static StringBuilder appendColumnsEqualPlaceholders(StringBuilder builder, String[] columns) { + for (int i = 0; i < columns.length; i++) { + appendColumn(builder, columns[i]).append("=?"); + if (i < columns.length - 1) { + builder.append(','); + } + } + return builder; + } + + public static StringBuilder appendColumnsEqValue(StringBuilder builder, String tableAlias, String[] columns) { + for (int i = 0; i < columns.length; i++) { + appendColumn(builder, tableAlias, columns[i]).append("=?"); + if (i < columns.length - 1) { + builder.append(','); + } + } + return builder; + } + + public static String createSqlInsert(String insertInto, String tablename, String[] columns) { + StringBuilder builder = new StringBuilder(insertInto); + builder.append('"').append(tablename).append('"').append(" ("); + appendColumns(builder, columns); + builder.append(") VALUES ("); + appendPlaceholders(builder, columns.length); + builder.append(')'); + return builder.toString(); + } + + /** Creates an select for given columns with a trailing space */ + public static String createSqlSelect(String tablename, String tableAlias, String[] columns, boolean distinct) { + if (tableAlias == null || tableAlias.length() < 0) { + throw new DaoException("Table alias required"); + } + + StringBuilder builder = new StringBuilder(distinct ? "SELECT DISTINCT " : "SELECT "); + SqlUtils.appendColumns(builder, tableAlias, columns).append(" FROM "); + builder.append('"').append(tablename).append('"').append(' ').append(tableAlias).append(' '); + return builder.toString(); + } + + /** Creates SELECT COUNT(*) with a trailing space. */ + public static String createSqlSelectCountStar(String tablename, String tableAliasOrNull) { + StringBuilder builder = new StringBuilder("SELECT COUNT(*) FROM "); + builder.append('"').append(tablename).append('"').append(' '); + if (tableAliasOrNull != null) { + builder.append(tableAliasOrNull).append(' '); + } + return builder.toString(); + } + + /** Remember: SQLite does not support joins nor table alias for DELETE. */ + public static String createSqlDelete(String tablename, String[] columns) { + String quotedTablename = '"' + tablename + '"'; + StringBuilder builder = new StringBuilder("DELETE FROM "); + builder.append(quotedTablename); + if (columns != null && columns.length > 0) { + builder.append(" WHERE "); + appendColumnsEqValue(builder, quotedTablename, columns); + } + return builder.toString(); + } + + public static String createSqlUpdate(String tablename, String[] updateColumns, String[] whereColumns) { + String quotedTablename = '"' + tablename + '"'; + StringBuilder builder = new StringBuilder("UPDATE "); + builder.append(quotedTablename).append(" SET "); + appendColumnsEqualPlaceholders(builder, updateColumns); + builder.append(" WHERE "); + appendColumnsEqValue(builder, quotedTablename, whereColumns); + return builder.toString(); + } + + public static String createSqlCount(String tablename) { + return "SELECT COUNT(*) FROM \"" + tablename +'"'; + } + + public static String escapeBlobArgument(byte[] bytes) { + return "X'" + toHex(bytes) + '\''; + } + + public static String toHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; i++) { + int byteValue = bytes[i] & 0xFF; + hexChars[i * 2] = HEX_ARRAY[byteValue >>> 4]; + hexChars[i * 2 + 1] = HEX_ARRAY[byteValue & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/internal/TableStatements.java b/greenDao/src/main/java/org/greenrobot/greendao/internal/TableStatements.java new file mode 100644 index 0000000..18aef15 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/internal/TableStatements.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.internal; + +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.database.DatabaseStatement; + +/** Helper class to create SQL statements for specific tables (used by greenDAO internally). */ +// Note: avoid locking while compiling any statement (accessing the db) to avoid deadlocks on lock-savvy DBs like +// SQLCipher. +public class TableStatements { + private final Database db; + private final String tablename; + private final String[] allColumns; + private final String[] pkColumns; + + private DatabaseStatement insertStatement; + private DatabaseStatement insertOrReplaceStatement; + private DatabaseStatement updateStatement; + private DatabaseStatement deleteStatement; + private DatabaseStatement countStatement; + + private volatile String selectAll; + private volatile String selectByKey; + private volatile String selectByRowId; + private volatile String selectKeys; + + public TableStatements(Database db, String tablename, String[] allColumns, String[] pkColumns) { + this.db = db; + this.tablename = tablename; + this.allColumns = allColumns; + this.pkColumns = pkColumns; + } + + public DatabaseStatement getInsertStatement() { + if (insertStatement == null) { + String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns); + DatabaseStatement newInsertStatement = db.compileStatement(sql); + synchronized (this) { + if (insertStatement == null) { + insertStatement = newInsertStatement; + } + } + if (insertStatement != newInsertStatement) { + newInsertStatement.close(); + } + } + return insertStatement; + } + + public DatabaseStatement getInsertOrReplaceStatement() { + if (insertOrReplaceStatement == null) { + String sql = SqlUtils.createSqlInsert("INSERT OR REPLACE INTO ", tablename, allColumns); + DatabaseStatement newInsertOrReplaceStatement = db.compileStatement(sql); + synchronized (this) { + if (insertOrReplaceStatement == null) { + insertOrReplaceStatement = newInsertOrReplaceStatement; + } + } + if (insertOrReplaceStatement != newInsertOrReplaceStatement) { + newInsertOrReplaceStatement.close(); + } + } + return insertOrReplaceStatement; + } + + public DatabaseStatement getDeleteStatement() { + if (deleteStatement == null) { + String sql = SqlUtils.createSqlDelete(tablename, pkColumns); + DatabaseStatement newDeleteStatement = db.compileStatement(sql); + synchronized (this) { + if (deleteStatement == null) { + deleteStatement = newDeleteStatement; + } + } + if (deleteStatement != newDeleteStatement) { + newDeleteStatement.close(); + } + } + return deleteStatement; + } + + public DatabaseStatement getUpdateStatement() { + if (updateStatement == null) { + String sql = SqlUtils.createSqlUpdate(tablename, allColumns, pkColumns); + DatabaseStatement newUpdateStatement = db.compileStatement(sql); + synchronized (this) { + if (updateStatement == null) { + updateStatement = newUpdateStatement; + } + } + if (updateStatement != newUpdateStatement) { + newUpdateStatement.close(); + } + } + return updateStatement; + } + + public DatabaseStatement getCountStatement() { + if (countStatement == null) { + String sql = SqlUtils.createSqlCount(tablename); + countStatement = db.compileStatement(sql); + } + return countStatement; + } + + /** ends with an space to simplify appending to this string. */ + public String getSelectAll() { + if (selectAll == null) { + selectAll = SqlUtils.createSqlSelect(tablename, "T", allColumns, false); + } + return selectAll; + } + + /** ends with an space to simplify appending to this string. */ + public String getSelectKeys() { + if (selectKeys == null) { + selectKeys = SqlUtils.createSqlSelect(tablename, "T", pkColumns, false); + } + return selectKeys; + } + + // TODO precompile + public String getSelectByKey() { + if (selectByKey == null) { + StringBuilder builder = new StringBuilder(getSelectAll()); + builder.append("WHERE "); + SqlUtils.appendColumnsEqValue(builder, "T", pkColumns); + selectByKey = builder.toString(); + } + return selectByKey; + } + + public String getSelectByRowId() { + if (selectByRowId == null) { + selectByRowId = getSelectAll() + "WHERE ROWID=?"; + } + return selectByRowId; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQuery.java b/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQuery.java new file mode 100644 index 0000000..e798e92 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQuery.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.InternalQueryDaoAccess; + +/** + * A repeatable query returning entities. + * + * @author Markus + * + * @param + * The entity class the query will return results for. + */ +// TODO support long, double and other types, not just Strings, for parameters +// TODO Make parameters setable by Property (if unique in parameters) +// TODO Make query compilable +abstract class AbstractQuery { + protected final AbstractDao dao; + protected final InternalQueryDaoAccess daoAccess; + protected final String sql; + protected final String[] parameters; + protected final Thread ownerThread; + + protected static String[] toStringArray(Object[] values) { + int length = values.length; + String[] strings = new String[length]; + for (int i = 0; i < length; i++) { + Object object = values[i]; + if (object != null) { + strings[i] = object.toString(); + } else { + strings[i] = null; + } + } + return strings; + } + + protected AbstractQuery(AbstractDao dao, String sql, String[] parameters) { + this.dao = dao; + this.daoAccess = new InternalQueryDaoAccess(dao); + this.sql = sql; + this.parameters = parameters; + ownerThread = Thread.currentThread(); + } + + // public void compile() { + // // TODO implement compile + // } + + /** + * Sets the parameter (0 based) using the position in which it was added during building the query. + */ + public AbstractQuery setParameter(int index, Object parameter) { + checkThread(); + if (parameter != null) { + parameters[index] = parameter.toString(); + } else { + parameters[index] = null; + } + return this; + } + + protected void checkThread() { + if (Thread.currentThread() != ownerThread) { + throw new DaoException( + "Method may be called only in owner thread, use forCurrentThread to get an instance for this thread"); + } + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQueryData.java b/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQueryData.java new file mode 100644 index 0000000..880370c --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQueryData.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.query; + +import org.greenrobot.greendao.AbstractDao; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +abstract class AbstractQueryData> { + final String sql; + final AbstractDao dao; + final String[] initialValues; + final Map> queriesForThreads; + + AbstractQueryData(AbstractDao dao, String sql, String[] initialValues) { + this.dao = dao; + this.sql = sql; + this.initialValues = initialValues; + queriesForThreads = new HashMap<>(); + } + + /** + * Just an optimized version, which performs faster if the current thread is already the query's owner thread. + * Note: all parameters are reset to their initial values specified in {@link QueryBuilder}. + */ + Q forCurrentThread(Q query) { + if (Thread.currentThread() == query.ownerThread) { + System.arraycopy(initialValues, 0, query.parameters, 0, initialValues.length); + return query; + } else { + return forCurrentThread(); + } + } + + /** + * Note: all parameters are reset to their initial values specified in {@link QueryBuilder}. + */ + Q forCurrentThread() { + // Process.myTid() seems to have issues on some devices (see Github #376) and Robolectric (#171): + // We use currentThread().getId() instead (unfortunately return a long, can not use SparseArray). + // PS.: thread ID may be reused, which should be fine because old thread will be gone anyway. + long threadId = Thread.currentThread().getId(); + synchronized (queriesForThreads) { + WeakReference queryRef = queriesForThreads.get(threadId); + Q query = queryRef != null ? queryRef.get() : null; + if (query == null) { + gc(); + query = createQuery(); + queriesForThreads.put(threadId, new WeakReference(query)); + } else { + System.arraycopy(initialValues, 0, query.parameters, 0, initialValues.length); + } + return query; + } + } + + abstract protected Q createQuery(); + + void gc() { + synchronized (queriesForThreads) { + Iterator>> iterator = queriesForThreads.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> entry = iterator.next(); + if (entry.getValue().get() == null) { + iterator.remove(); + } + } + } + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQueryWithLimit.java b/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQueryWithLimit.java new file mode 100644 index 0000000..ddf1827 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/AbstractQueryWithLimit.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import org.greenrobot.greendao.AbstractDao; + +import java.util.Date; + +/** + * Base class for queries returning data (entities or cursor). + * + * @param The entity class the query will return results for. + * @author Markus + */ +// TODO Query for PKs/ROW IDs +abstract class AbstractQueryWithLimit extends AbstractQuery { + protected final int limitPosition; + protected final int offsetPosition; + + protected AbstractQueryWithLimit(AbstractDao dao, String sql, String[] initialValues, int limitPosition, + int offsetPosition) { + super(dao, sql, initialValues); + this.limitPosition = limitPosition; + this.offsetPosition = offsetPosition; + } + + /** + * Sets the parameter (0 based) using the position in which it was added during building the query. Note: all + * standard WHERE parameters come first. After that come the WHERE parameters of joins (if any). + */ + public AbstractQueryWithLimit setParameter(int index, Object parameter) { + if (index >= 0 && (index == limitPosition || index == offsetPosition)) { + throw new IllegalArgumentException("Illegal parameter index: " + index); + } + return (AbstractQueryWithLimit) super.setParameter(index, parameter); + } + + public AbstractQueryWithLimit setParameter(int index, Date parameter) { + Long converted = parameter != null ? parameter.getTime() : null; + return setParameter(index, converted); + } + + public AbstractQueryWithLimit setParameter(int index, Boolean parameter) { + Integer converted = parameter != null ? (parameter ? 1 : 0) : null; + return setParameter(index, converted); + } + + /** + * Sets the limit of the maximum number of results returned by this Query. {@link + * org.greenrobot.greendao.query.QueryBuilder#limit(int)} must + * have been called on the QueryBuilder that created this Query object. + */ + public void setLimit(int limit) { + checkThread(); + if (limitPosition == -1) { + throw new IllegalStateException("Limit must be set with QueryBuilder before it can be used here"); + } + parameters[limitPosition] = Integer.toString(limit); + } + + /** + * Sets the offset for results returned by this Query. {@link org.greenrobot.greendao.query.QueryBuilder#offset(int)} must + * have been called on + * the QueryBuilder that created this Query object. + */ + public void setOffset(int offset) { + checkThread(); + if (offsetPosition == -1) { + throw new IllegalStateException("Offset must be set with QueryBuilder before it can be used here"); + } + parameters[offsetPosition] = Integer.toString(offset); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/CloseableListIterator.java b/greenDao/src/main/java/org/greenrobot/greendao/query/CloseableListIterator.java new file mode 100644 index 0000000..b74ab49 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/CloseableListIterator.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import java.io.Closeable; +import java.util.ListIterator; + +/** + * A list iterator that needs to be closed (or the associated list) to free underlying resources like a database cursor. + * Typically used with LazyList. + * + * @author Markus + * + * @param + */ +public interface CloseableListIterator extends ListIterator, Closeable { + +} \ No newline at end of file diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/CountQuery.java b/greenDao/src/main/java/org/greenrobot/greendao/query/CountQuery.java new file mode 100644 index 0000000..5a4a493 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/CountQuery.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.query; + +import android.database.Cursor; +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoException; + +public class CountQuery extends AbstractQuery { + + private final static class QueryData extends AbstractQueryData> { + + private QueryData(AbstractDao dao, String sql, String[] initialValues) { + super(dao, sql, initialValues); + } + + @Override + protected CountQuery createQuery() { + return new CountQuery(this, dao, sql, initialValues.clone()); + } + } + + static CountQuery create(AbstractDao dao, String sql, Object[] initialValues) { + QueryData queryData = new QueryData(dao, sql, toStringArray(initialValues)); + return queryData.forCurrentThread(); + } + + private final QueryData queryData; + + private CountQuery(QueryData queryData, AbstractDao dao, String sql, String[] initialValues) { + super(dao, sql, initialValues); + this.queryData = queryData; + } + + public CountQuery forCurrentThread() { + return queryData.forCurrentThread(this); + } + + /** Returns the count (number of results matching the query). Uses SELECT COUNT (*) sematics. */ + public long count() { + checkThread(); + Cursor cursor = dao.getDatabase().rawQuery(sql, parameters); + try { + if (!cursor.moveToNext()) { + throw new DaoException("No result for count"); + } else if (!cursor.isLast()) { + throw new DaoException("Unexpected row count: " + cursor.getCount()); + } else if (cursor.getColumnCount() != 1) { + throw new DaoException("Unexpected column count: " + cursor.getColumnCount()); + } + return cursor.getLong(0); + } finally { + cursor.close(); + } + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/CursorQuery.java b/greenDao/src/main/java/org/greenrobot/greendao/query/CursorQuery.java new file mode 100644 index 0000000..ae6dee3 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/CursorQuery.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import android.database.Cursor; +import org.greenrobot.greendao.AbstractDao; + +/** + * A repeatable query returning a raw android.database.Cursor. Note, that using cursors is usually a hassle and + * greenDAO provides a higher level abstraction using entities (see {@link org.greenrobot.greendao.query.Query}). This class + * can nevertheless be useful to work with legacy code that is based on Cursors or CursorLoaders. + * + * @param The entity class the query will return results for. + * @author Markus + */ +public class CursorQuery extends AbstractQueryWithLimit { + private final static class QueryData extends AbstractQueryData> { + private final int limitPosition; + private final int offsetPosition; + + QueryData(AbstractDao dao, String sql, String[] initialValues, int limitPosition, int offsetPosition) { + super(dao, sql, initialValues); + this.limitPosition = limitPosition; + this.offsetPosition = offsetPosition; + } + + @Override + protected CursorQuery createQuery() { + return new CursorQuery(this, dao, sql, initialValues.clone(), limitPosition, offsetPosition); + } + + } + + /** For internal use by greenDAO only. */ + public static CursorQuery internalCreate(AbstractDao dao, String sql, Object[] initialValues) { + return create(dao, sql, initialValues, -1, -1); + } + + static CursorQuery create(AbstractDao dao, String sql, Object[] initialValues, int limitPosition, + int offsetPosition) { + QueryData queryData = new QueryData(dao, sql, toStringArray(initialValues), limitPosition, + offsetPosition); + return queryData.forCurrentThread(); + } + + private final QueryData queryData; + + private CursorQuery(QueryData queryData, AbstractDao dao, String sql, String[] initialValues, int limitPosition, + int offsetPosition) { + super(dao, sql, initialValues, limitPosition, offsetPosition); + this.queryData = queryData; + } + + public CursorQuery forCurrentThread() { + return queryData.forCurrentThread(this); + } + + /** Executes the query and returns a raw android.database.Cursor. Don't forget to close it. */ + public Cursor query() { + checkThread(); + return dao.getDatabase().rawQuery(sql, parameters); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/DeleteQuery.java b/greenDao/src/main/java/org/greenrobot/greendao/query/DeleteQuery.java new file mode 100644 index 0000000..656ebe4 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/DeleteQuery.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.database.Database; + +/** + * A repeatable query for deleting entities.
    + * New API note: this is more likely to change. + * + * @param The entity class the query will delete from. + * @author Markus + */ +public class DeleteQuery extends AbstractQuery { + private final static class QueryData extends AbstractQueryData> { + + private QueryData(AbstractDao dao, String sql, String[] initialValues) { + super(dao, sql, initialValues); + } + + @Override + protected DeleteQuery createQuery() { + return new DeleteQuery(this, dao, sql, initialValues.clone()); + } + } + + static DeleteQuery create(AbstractDao dao, String sql, Object[] initialValues) { + QueryData queryData = new QueryData(dao, sql, toStringArray(initialValues)); + return queryData.forCurrentThread(); + } + + private final QueryData queryData; + + private DeleteQuery(QueryData queryData, AbstractDao dao, String sql, String[] initialValues) { + super(dao, sql, initialValues); + this.queryData = queryData; + } + + public DeleteQuery forCurrentThread() { + return queryData.forCurrentThread(this); + } + + /** + * Deletes all matching entities without detaching them from the identity scope (aka session/cache). Note that this + * method may lead to stale entity objects in the session cache. Stale entities may be returned when loaded by + * their + * primary key, but not using queries. + */ + public void executeDeleteWithoutDetachingEntities() { + checkThread(); + Database db = dao.getDatabase(); + if (db.isDbLockedByCurrentThread()) { + dao.getDatabase().execSQL(sql, parameters); + } else { + // Do TX to acquire a connection before locking this to avoid deadlocks + // Locking order as described in AbstractDao + db.beginTransaction(); + try { + dao.getDatabase().execSQL(sql, parameters); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/Join.java b/greenDao/src/main/java/org/greenrobot/greendao/query/Join.java new file mode 100644 index 0000000..7012798 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/Join.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.Property; + +/** + * A Join lets you relate to other entity types for queries, and allows using WHERE statements on the joined entity + * type. + */ +public class Join { + + final String sourceTablePrefix; + final AbstractDao daoDestination; + + final Property joinPropertySource; + final Property joinPropertyDestination; + final String tablePrefix; + final WhereCollector whereCollector; + + public Join(String sourceTablePrefix, Property sourceJoinProperty, + AbstractDao daoDestination, Property destinationJoinProperty, + String joinTablePrefix) { + this.sourceTablePrefix = sourceTablePrefix; + this.joinPropertySource = sourceJoinProperty; + this.daoDestination = daoDestination; + this.joinPropertyDestination = destinationJoinProperty; + tablePrefix = joinTablePrefix; + whereCollector = new WhereCollector(daoDestination, joinTablePrefix); + } + + + /** + * Adds the given conditions to the where clause using an logical AND. To create new conditions, use the properties + * given in the generated dao classes. + */ + public Join where(WhereCondition cond, WhereCondition... condMore) { + whereCollector.add(cond, condMore); + return this; + } + + /** + * Adds the given conditions to the where clause using an logical OR. To create new conditions, use the properties + * given in the generated dao classes. + */ + public Join whereOr(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore) { + whereCollector.add(or(cond1, cond2, condMore)); + return this; + } + + /** + * Creates a WhereCondition by combining the given conditions using OR. The returned WhereCondition must be used + * inside {@link #where(WhereCondition, WhereCondition...)} or + * {@link #whereOr(WhereCondition, WhereCondition, WhereCondition...)}. + */ + public WhereCondition or(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore) { + return whereCollector.combineWhereConditions(" OR ", cond1, cond2, condMore); + } + + /** + * Creates a WhereCondition by combining the given conditions using AND. The returned WhereCondition must be used + * inside {@link #where(WhereCondition, WhereCondition...)} or + * {@link #whereOr(WhereCondition, WhereCondition, WhereCondition...)}. + */ + public WhereCondition and(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore) { + return whereCollector.combineWhereConditions(" AND ", cond1, cond2, condMore); + } + + /** + * Usually you don't need this value; just in case you are mixing custom + * {@link org.greenrobot.greendao.query.WhereCondition.StringCondition} into the query, this value allows to reference + * the joined (target) table. + */ + public String getTablePrefix() { + return tablePrefix; + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/LazyList.java b/greenDao/src/main/java/org/greenrobot/greendao/query/LazyList.java new file mode 100644 index 0000000..aee827b --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/LazyList.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.concurrent.locks.ReentrantLock; + +import android.database.Cursor; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.InternalQueryDaoAccess; + +/** + * A thread-safe, unmodifiable list that reads entities once they are accessed from an underlying database cursor. Make + * sure to close the list once you are done with it. The lazy list can be cached or not. Cached lazy lists store the + * entities in memory to avoid loading entities more than once. Some features of the list are limited to cached lists + * (e.g. features that require the entire list). Cached lists close the cursor automatically once you queried all + * entities. However, to avoid leaked cursors, you should not rely on this behavior: if an exception occurs before the + * entire list is read, you should close the lazy list (and thus the underlying cursor) on your own to be on the safe + * side. + * + * @author Markus + * + * @param + * Entity type. + */ +public class LazyList implements List, Closeable { + protected class LazyIterator implements CloseableListIterator { + private int index; + private final boolean closeWhenDone; + + public LazyIterator(int startLocation, boolean closeWhenDone) { + index = startLocation; + this.closeWhenDone = closeWhenDone; + } + + @Override + public void add(E object) { + throw new UnsupportedOperationException(); + } + + @Override + /** FIXME: before hasPrevious(), next() must be called. */ + public boolean hasPrevious() { + return index > 0; + } + + @Override + public int nextIndex() { + return index; + } + + @Override + /** FIXME: before previous(), next() must be called. */ + public E previous() { + if (index <= 0) { + throw new NoSuchElementException(); + } + index--; + E entity = get(index); + // if (index == size && closeWhenDone) { + // close(); + // } + return entity; + } + + @Override + public int previousIndex() { + return index - 1; + } + + @Override + public void set(E object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public E next() { + if (index >= size) { + throw new NoSuchElementException(); + } + E entity = get(index); + index++; + if (index == size && closeWhenDone) { + close(); + } + return entity; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + LazyList.this.close(); + } + + } + + private final InternalQueryDaoAccess daoAccess; + private final Cursor cursor; + private final List entities; + private final int size; + private final ReentrantLock lock; + private volatile int loadedCount; + + LazyList(InternalQueryDaoAccess daoAccess, Cursor cursor, boolean cacheEntities) { + this.cursor = cursor; + this.daoAccess = daoAccess; + size = cursor.getCount(); + if (cacheEntities) { + entities = new ArrayList(size); + for (int i = 0; i < size; i++) { + entities.add(null); + } + } else { + entities = null; + } + if (size == 0) { + cursor.close(); + } + + lock = new ReentrantLock(); + } + + /** Loads the remaining entities (if any) that were not loaded before. Applies to cached lazy lists only. */ + public void loadRemaining() { + checkCached(); + int size = entities.size(); + for (int i = 0; i < size; i++) { + get(i); + } + } + + protected void checkCached() { + if (entities == null) { + throw new DaoException("This operation only works with cached lazy lists"); + } + } + + /** Like get but does not load the entity if it was not loaded before. */ + public E peak(int location) { + if (entities != null) { + return entities.get(location); + } else { + return null; + } + } + + @Override + /** Closes the underlying cursor: do not try to get entities not loaded (using get) before. */ + public void close() { + cursor.close(); + } + + public boolean isClosed() { + return cursor.isClosed(); + } + + public int getLoadedCount() { + return loadedCount; + } + + public boolean isLoadedCompletely() { + return loadedCount == size; + } + + @Override + public boolean add(E object) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int location, E object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection arg0) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(int arg0, Collection arg1) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(Object object) { + loadRemaining(); + return entities.contains(object); + } + + @Override + public boolean containsAll(Collection collection) { + loadRemaining(); + return entities.containsAll(collection); + } + + @Override + public E get(int location) { + if (entities != null) { + E entity = entities.get(location); + if (entity == null) { + lock.lock(); + try { + entity = entities.get(location); + if (entity == null) { + entity = loadEntity(location); + entities.set(location, entity); + // Ignore FindBugs: increment of volatile is fine here because we use a lock + loadedCount++; + if (loadedCount == size) { + cursor.close(); + } + } + } finally { + lock.unlock(); + } + } + return entity; + } else { + lock.lock(); + try { + return loadEntity(location); + } finally { + lock.unlock(); + } + } + } + + /** Lock must be locked when entering this method. */ + protected E loadEntity(int location) { + boolean ok = cursor.moveToPosition(location); + if(!ok) { + throw new DaoException("Could not move to cursor location " + location); + } + E entity = daoAccess.loadCurrent(cursor, 0, true); + if (entity == null) { + throw new DaoException("Loading of entity failed (null) at position " + location); + } + return entity; + } + + @Override + public int indexOf(Object object) { + loadRemaining(); + return entities.indexOf(object); + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public Iterator iterator() { + return new LazyIterator(0, false); + } + + @Override + public int lastIndexOf(Object object) { + loadRemaining(); + return entities.lastIndexOf(object); + } + + @Override + public CloseableListIterator listIterator() { + return new LazyIterator(0, false); + } + + /** Closes this list's cursor once the iterator is fully iterated through. */ + public CloseableListIterator listIteratorAutoClose() { + return new LazyIterator(0, true); + } + + @Override + public ListIterator listIterator(int location) { + return new LazyIterator(location, false); + } + + @Override + public E remove(int location) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection arg0) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection arg0) { + throw new UnsupportedOperationException(); + } + + @Override + public E set(int location, E object) { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return size; + } + + @Override + public List subList(int start, int end) { + checkCached(); + for (int i = start; i < end; i++) { + get(i); + } + return entities.subList(start, end); + } + + @Override + public Object[] toArray() { + loadRemaining(); + return entities.toArray(); + } + + @Override + public T[] toArray(T[] array) { + loadRemaining(); + return entities.toArray(array); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/Query.java b/greenDao/src/main/java/org/greenrobot/greendao/query/Query.java new file mode 100644 index 0000000..a9834ec --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/Query.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import android.database.Cursor; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.annotation.apihint.Internal; +import org.greenrobot.greendao.rx.RxQuery; +import org.greenrobot.greendao.rx.RxTransaction; + +import java.util.Date; +import java.util.List; + +import io.reactivex.schedulers.Schedulers; + + +/** + * A repeatable query returning entities. + * + * @param The entity class the query will return results for. + * @author Markus + */ +public class Query extends AbstractQueryWithLimit { + private final static class QueryData extends AbstractQueryData> { + private final int limitPosition; + private final int offsetPosition; + + QueryData(AbstractDao dao, String sql, String[] initialValues, int limitPosition, int offsetPosition) { + super(dao, sql, initialValues); + this.limitPosition = limitPosition; + this.offsetPosition = offsetPosition; + } + + @Override + protected Query createQuery() { + return new Query(this, dao, sql, initialValues.clone(), limitPosition, offsetPosition); + } + + } + + /** For internal use by greenDAO only. */ + public static Query internalCreate(AbstractDao dao, String sql, Object[] initialValues) { + return create(dao, sql, initialValues, -1, -1); + } + + static Query create(AbstractDao dao, String sql, Object[] initialValues, int limitPosition, + int offsetPosition) { + QueryData queryData = new QueryData(dao, sql, toStringArray(initialValues), limitPosition, + offsetPosition); + return queryData.forCurrentThread(); + } + + private final QueryData queryData; + + private volatile RxQuery rxTxPlain; + private volatile RxQuery rxTxIo; + + private Query(QueryData queryData, AbstractDao dao, String sql, String[] initialValues, int limitPosition, + int offsetPosition) { + super(dao, sql, initialValues, limitPosition, offsetPosition); + this.queryData = queryData; + } + + /** + * Note: all parameters are reset to their initial values specified in {@link QueryBuilder}. + */ + public Query forCurrentThread() { + return queryData.forCurrentThread(this); + } + + /** Executes the query and returns the result as a list containing all entities loaded into memory. */ + public List list() { + checkThread(); + Cursor cursor = dao.getDatabase().rawQuery(sql, parameters); + return daoAccess.loadAllAndCloseCursor(cursor); + } + + /** + * Executes the query and returns the result as a list that lazy loads the entities on first access. Entities are + * cached, so accessing the same entity more than once will not result in loading an entity from the underlying + * cursor again.Make sure to close it to close the underlying cursor. + */ + public LazyList listLazy() { + checkThread(); + Cursor cursor = dao.getDatabase().rawQuery(sql, parameters); + return new LazyList(daoAccess, cursor, true); + } + + /** + * Executes the query and returns the result as a list that lazy loads the entities on every access (uncached). + * Make sure to close the list to close the underlying cursor. + */ + public LazyList listLazyUncached() { + checkThread(); + Cursor cursor = dao.getDatabase().rawQuery(sql, parameters); + return new LazyList(daoAccess, cursor, false); + } + + /** + * Executes the query and returns the result as a list iterator; make sure to close it to close the underlying + * cursor. The cursor is closed once the iterator is fully iterated through. + */ + public CloseableListIterator listIterator() { + return listLazyUncached().listIteratorAutoClose(); + } + + /** + * Executes the query and returns the unique result or null. + * + * @return Entity or null if no matching entity was found + * @throws DaoException if the result is not unique + */ + public T unique() { + checkThread(); + Cursor cursor = dao.getDatabase().rawQuery(sql, parameters); + return daoAccess.loadUniqueAndCloseCursor(cursor); + } + + /** + * Executes the query and returns the unique result (never null). + * + * @return Entity + * @throws DaoException if the result is not unique or no entity was found + */ + public T uniqueOrThrow() { + T entity = unique(); + if (entity == null) { + throw new DaoException("No entity found for query"); + } + return entity; + } + + @Override + public Query setParameter(int index, Object parameter) { + return (Query) super.setParameter(index, parameter); + } + + @Override + public Query setParameter(int index, Date parameter) { + return (Query) super.setParameter(index, parameter); + } + + @Override + public Query setParameter(int index, Boolean parameter) { + return (Query) super.setParameter(index, parameter); + } + + /** + * DO NOT USE. + * The returned {@link RxTransaction} allows getting query results using Rx Observables without any Scheduler set + * for subscribeOn. + * + * @see #__InternalRx() + */ + @Internal + public RxQuery __internalRxPlain() { + if (rxTxPlain == null) { + rxTxPlain = new RxQuery(this); + } + return rxTxPlain; + } + + /** + * DO NOT USE. + * The returned {@link RxTransaction} allows getting query results using Rx Observables using RX's IO scheduler for + * subscribeOn. + * + * @see #__internalRxPlain() + */ + @Internal + public RxQuery __InternalRx() { + if (rxTxIo == null) { + rxTxIo = new RxQuery(this, Schedulers.io()); + } + return rxTxIo; + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/QueryBuilder.java b/greenDao/src/main/java/org/greenrobot/greendao/query/QueryBuilder.java new file mode 100644 index 0000000..a0667c0 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/QueryBuilder.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.query; + +import android.database.sqlite.SQLiteDatabase; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.AbstractDaoSession; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.DaoLog; +import org.greenrobot.greendao.Property; +import org.greenrobot.greendao.annotation.apihint.Experimental; +import org.greenrobot.greendao.internal.SqlUtils; +import org.greenrobot.greendao.rx.RxQuery; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builds custom entity queries using constraints and parameters and without SQL (QueryBuilder creates SQL for you). To + * acquire an QueryBuilder, use {@link AbstractDao#queryBuilder()} or {@link AbstractDaoSession#queryBuilder(Class)}. + * Entity properties are referenced by Fields in the "Properties" inner class of the generated DAOs. This approach + * allows compile time checks and prevents typo errors occuring at build time.
    + *
    + * Example: Query for all users with the first name "Joe" ordered by their last name. (The class Properties is an inner + * class of UserDao and should be imported before.)
    + * + * List joes = dao.queryBuilder().where(Properties.FirstName.eq("Joe")).orderAsc(Properties.LastName).list(); + * + * + * @param Entity class to create an query for. + * @author Markus + */ +public class QueryBuilder { + + /** Set to true to debug the SQL. */ + public static boolean LOG_SQL; + + /** Set to see the given values. */ + public static boolean LOG_VALUES; + private final WhereCollector whereCollector; + + private StringBuilder orderBuilder; + + private final List values; + private final List> joins; + private final AbstractDao dao; + private final String tablePrefix; + + private Integer limit; + private Integer offset; + private boolean distinct; + + /** stored with a leading space */ + private String stringOrderCollation; + + /** For internal use by greenDAO only. */ + public static QueryBuilder internalCreate(AbstractDao dao) { + return new QueryBuilder(dao); + } + + protected QueryBuilder(AbstractDao dao) { + this(dao, "T"); + } + + protected QueryBuilder(AbstractDao dao, String tablePrefix) { + this.dao = dao; + this.tablePrefix = tablePrefix; + values = new ArrayList(); + joins = new ArrayList>(); + whereCollector = new WhereCollector(dao, tablePrefix); + stringOrderCollation = " COLLATE NOCASE"; + } + + private void checkOrderBuilder() { + if (orderBuilder == null) { + orderBuilder = new StringBuilder(); + } else if (orderBuilder.length() > 0) { + orderBuilder.append(","); + } + } + + /** Use a SELECT DISTINCT to avoid duplicate entities returned, e.g. when doing joins. */ + public QueryBuilder distinct() { + distinct = true; + return this; + } + + /** + * If using Android's embedded SQLite, this enables localized ordering of strings + * (see {@link #orderAsc(Property...)} and {@link #orderDesc(Property...)}). This uses "COLLATE LOCALIZED", which + * is unavailable in SQLCipher (in that case, the ordering is unchanged). + * + * @see #stringOrderCollation + */ + public QueryBuilder preferLocalizedStringOrder() { + // SQLCipher 3.5.0+ does not understand "COLLATE LOCALIZED" + if (dao.getDatabase().getRawDatabase() instanceof SQLiteDatabase) { + stringOrderCollation = " COLLATE LOCALIZED"; + } + return this; + } + + /** + * Customizes the ordering of strings used by {@link #orderAsc(Property...)} and {@link #orderDesc(Property...)}. + * Default is "COLLATE NOCASE". + * + * @see #preferLocalizedStringOrder + */ + public QueryBuilder stringOrderCollation(String stringOrderCollation) { + // SQLCipher 3.5.0+ does not understand "COLLATE LOCALIZED" + if (dao.getDatabase().getRawDatabase() instanceof SQLiteDatabase) { + this.stringOrderCollation = stringOrderCollation == null || stringOrderCollation.startsWith(" ") ? + stringOrderCollation : " " + stringOrderCollation; + } + return this; + } + + /** + * Adds the given conditions to the where clause using an logical AND. To create new conditions, use the properties + * given in the generated dao classes. + */ + public QueryBuilder where(WhereCondition cond, WhereCondition... condMore) { + whereCollector.add(cond, condMore); + return this; + } + + /** + * Adds the given conditions to the where clause using an logical OR. To create new conditions, use the properties + * given in the generated dao classes. + */ + public QueryBuilder whereOr(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore) { + whereCollector.add(or(cond1, cond2, condMore)); + return this; + } + + /** + * Creates a WhereCondition by combining the given conditions using OR. The returned WhereCondition must be used + * inside {@link #where(WhereCondition, WhereCondition...)} or + * {@link #whereOr(WhereCondition, WhereCondition, WhereCondition...)}. + */ + public WhereCondition or(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore) { + return whereCollector.combineWhereConditions(" OR ", cond1, cond2, condMore); + } + + /** + * Creates a WhereCondition by combining the given conditions using AND. The returned WhereCondition must be used + * inside {@link #where(WhereCondition, WhereCondition...)} or + * {@link #whereOr(WhereCondition, WhereCondition, WhereCondition...)}. + */ + public WhereCondition and(WhereCondition cond1, WhereCondition cond2, WhereCondition... condMore) { + return whereCollector.combineWhereConditions(" AND ", cond1, cond2, condMore); + } + + /** + * Expands the query to another entity type by using a JOIN. The primary key property of the primary entity for + * this QueryBuilder is used to match the given destinationProperty. + */ + public Join join(Class destinationEntityClass, Property destinationProperty) { + return join(dao.getPkProperty(), destinationEntityClass, destinationProperty); + } + + /** + * Expands the query to another entity type by using a JOIN. The given sourceProperty is used to match the primary + * key property of the given destinationEntity. + */ + public Join join(Property sourceProperty, Class destinationEntityClass) { + AbstractDao destinationDao = (AbstractDao) dao.getSession().getDao(destinationEntityClass); + Property destinationProperty = destinationDao.getPkProperty(); + return addJoin(tablePrefix, sourceProperty, destinationDao, destinationProperty); + } + + /** + * Expands the query to another entity type by using a JOIN. The given sourceProperty is used to match the given + * destinationProperty of the given destinationEntity. + */ + public Join join(Property sourceProperty, Class destinationEntityClass, Property destinationProperty) { + AbstractDao destinationDao = (AbstractDao) dao.getSession().getDao(destinationEntityClass); + return addJoin(tablePrefix, sourceProperty, destinationDao, destinationProperty); + } + + /** + * Expands the query to another entity type by using a JOIN. The given sourceJoin's property is used to match the + * given destinationProperty of the given destinationEntity. Note that destination entity of the given join is used + * as the source for the new join to add. In this way, it is possible to compose complex "join of joins" across + * several entities if required. + */ + public Join join(Join sourceJoin, Property sourceProperty, Class destinationEntityClass, + Property destinationProperty) { + AbstractDao destinationDao = (AbstractDao) dao.getSession().getDao(destinationEntityClass); + return addJoin(sourceJoin.tablePrefix, sourceProperty, destinationDao, destinationProperty); + } + + private Join addJoin(String sourceTablePrefix, Property sourceProperty, AbstractDao destinationDao, + Property destinationProperty) { + String joinTablePrefix = "J" + (joins.size() + 1); + Join join = new Join(sourceTablePrefix, sourceProperty, destinationDao, destinationProperty, + joinTablePrefix); + joins.add(join); + return join; + } + + /** Adds the given properties to the ORDER BY section using ascending order. */ + public QueryBuilder orderAsc(Property... properties) { + orderAscOrDesc(" ASC", properties); + return this; + } + + /** Adds the given properties to the ORDER BY section using descending order. */ + public QueryBuilder orderDesc(Property... properties) { + orderAscOrDesc(" DESC", properties); + return this; + } + + private void orderAscOrDesc(String ascOrDescWithLeadingSpace, Property... properties) { + for (Property property : properties) { + checkOrderBuilder(); + append(orderBuilder, property); + if (String.class.equals(property.type) && stringOrderCollation != null) { + orderBuilder.append(stringOrderCollation); + } + orderBuilder.append(ascOrDescWithLeadingSpace); + } + } + + /** Adds the given properties to the ORDER BY section using the given custom order. */ + public QueryBuilder orderCustom(Property property, String customOrderForProperty) { + checkOrderBuilder(); + append(orderBuilder, property).append(' '); + orderBuilder.append(customOrderForProperty); + return this; + } + + /** + * Adds the given raw SQL string to the ORDER BY section. Do not use this for standard properties: orderAsc and + * orderDesc are preferred. + */ + public QueryBuilder orderRaw(String rawOrder) { + checkOrderBuilder(); + orderBuilder.append(rawOrder); + return this; + } + + protected StringBuilder append(StringBuilder builder, Property property) { + whereCollector.checkProperty(property); + builder.append(tablePrefix).append('.').append('\'').append(property.columnName).append('\''); + return builder; + } + + + /** Limits the number of results returned by queries. */ + public QueryBuilder limit(int limit) { + this.limit = limit; + return this; + } + + /** + * Sets the offset for query results in combination with {@link #limit(int)}. The first {@code offset} results are + * skipped and the total number of results will be limited by {@code limit}. You cannot use offset without limit. + */ + public QueryBuilder offset(int offset) { + this.offset = offset; + return this; + } + + /** + * Builds a reusable query object (Query objects can be executed more efficiently than creating a QueryBuilder for + * each execution. + */ + public Query build() { + StringBuilder builder = createSelectBuilder(); + int limitPosition = checkAddLimit(builder); + int offsetPosition = checkAddOffset(builder); + + String sql = builder.toString(); + checkLog(sql); + + return Query.create(dao, sql, values.toArray(), limitPosition, offsetPosition); + } + + /** + * Builds a reusable query object for low level android.database.Cursor access. + * (Query objects can be executed more efficiently than creating a QueryBuilder for each execution. + */ + public CursorQuery buildCursor() { + StringBuilder builder = createSelectBuilder(); + int limitPosition = checkAddLimit(builder); + int offsetPosition = checkAddOffset(builder); + + String sql = builder.toString(); + checkLog(sql); + + return CursorQuery.create(dao, sql, values.toArray(), limitPosition, offsetPosition); + } + + private StringBuilder createSelectBuilder() { + String select = SqlUtils.createSqlSelect(dao.getTablename(), tablePrefix, dao.getAllColumns(), distinct); + StringBuilder builder = new StringBuilder(select); + + appendJoinsAndWheres(builder, tablePrefix); + + if (orderBuilder != null && orderBuilder.length() > 0) { + builder.append(" ORDER BY ").append(orderBuilder); + } + return builder; + } + + private int checkAddLimit(StringBuilder builder) { + int limitPosition = -1; + if (limit != null) { + builder.append(" LIMIT ?"); + values.add(limit); + limitPosition = values.size() - 1; + } + return limitPosition; + } + + private int checkAddOffset(StringBuilder builder) { + int offsetPosition = -1; + if (offset != null) { + if (limit == null) { + throw new IllegalStateException("Offset cannot be set without limit"); + } + builder.append(" OFFSET ?"); + values.add(offset); + offsetPosition = values.size() - 1; + } + return offsetPosition; + } + + /** + * Builds a reusable query object for deletion (Query objects can be executed more efficiently than creating a + * QueryBuilder for each execution. + */ + public DeleteQuery buildDelete() { + if (!joins.isEmpty()) { + throw new DaoException("JOINs are not supported for DELETE queries"); + } + String tablename = dao.getTablename(); + String baseSql = SqlUtils.createSqlDelete(tablename, null); + StringBuilder builder = new StringBuilder(baseSql); + + // tablePrefix gets replaced by table name below. Don't use tableName here because it causes trouble when + // table name ends with tablePrefix. + appendJoinsAndWheres(builder, tablePrefix); + + String sql = builder.toString(); + // Remove table aliases, not supported for DELETE queries. + // TODO(?): don't create table aliases in the first place. + sql = sql.replace(tablePrefix + ".\"", '"' + tablename + "\".\""); + checkLog(sql); + + return DeleteQuery.create(dao, sql, values.toArray()); + } + + /** + * Builds a reusable query object for counting rows (Query objects can be executed more efficiently than creating a + * QueryBuilder for each execution. + */ + public CountQuery buildCount() { + String tablename = dao.getTablename(); + String baseSql = SqlUtils.createSqlSelectCountStar(tablename, tablePrefix); + StringBuilder builder = new StringBuilder(baseSql); + appendJoinsAndWheres(builder, tablePrefix); + + String sql = builder.toString(); + checkLog(sql); + + return CountQuery.create(dao, sql, values.toArray()); + } + + private void checkLog(String sql) { + if (LOG_SQL) { + DaoLog.d("Built SQL for query: " + sql); + } + if (LOG_VALUES) { + DaoLog.d("Values for query: " + values); + } + } + + private void appendJoinsAndWheres(StringBuilder builder, String tablePrefixOrNull) { + values.clear(); + for (Join join : joins) { + builder.append(" JOIN ").append(join.daoDestination.getTablename()).append(' '); + builder.append(join.tablePrefix).append(" ON "); + SqlUtils.appendProperty(builder, join.sourceTablePrefix, join.joinPropertySource).append('='); + SqlUtils.appendProperty(builder, join.tablePrefix, join.joinPropertyDestination); + } + boolean whereAppended = !whereCollector.isEmpty(); + if (whereAppended) { + builder.append(" WHERE "); + whereCollector.appendWhereClause(builder, tablePrefixOrNull, values); + } + for (Join join : joins) { + if (!join.whereCollector.isEmpty()) { + if (!whereAppended) { + builder.append(" WHERE "); + whereAppended = true; + } else { + builder.append(" AND "); + } + join.whereCollector.appendWhereClause(builder, join.tablePrefix, values); + } + } + } + + /** + * Shorthand for {@link QueryBuilder#build() build()}.{@link Query#list() list()}; see {@link Query#list()} for + * details. To execute a query more than once, you should build the query and keep the {@link Query} object for + * efficiency reasons. + */ + public List list() { + return build().list(); + } + + /** + * Shorthand for {@link QueryBuilder#build() build()}.{@link Query#__InternalRx()}. + */ + @Experimental + public RxQuery rx() { + return build().__InternalRx(); + } + + /** + * Shorthand for {@link QueryBuilder#build() build()}.{@link Query#__internalRxPlain()}. + */ + @Experimental + public RxQuery rxPlain() { + return build().__internalRxPlain(); + } + + /** + * Shorthand for {@link QueryBuilder#build() build()}.{@link Query#listLazy() listLazy()}; see + * {@link Query#listLazy()} for details. To execute a query more than once, you should build the query and keep the + * {@link Query} object for efficiency reasons. + */ + public LazyList listLazy() { + return build().listLazy(); + } + + /** + * Shorthand for {@link QueryBuilder#build() build()}.{@link Query#listLazyUncached() listLazyUncached()}; see + * {@link Query#listLazyUncached()} for details. To execute a query more than once, you should build the query and + * keep the {@link Query} object for efficiency reasons. + */ + public LazyList listLazyUncached() { + return build().listLazyUncached(); + } + + /** + * Shorthand for {@link QueryBuilder#build() build()}.{@link Query#listIterator() listIterator()}; see + * {@link Query#listIterator()} for details. To execute a query more than once, you should build the query and keep + * the {@link Query} object for efficiency reasons. + */ + public CloseableListIterator listIterator() { + return build().listIterator(); + } + + /** + * Shorthand for {@link QueryBuilder#build() build()}.{@link Query#unique() unique()}; see {@link Query#unique()} + * for details. To execute a query more than once, you should build the query and keep the {@link Query} object for + * efficiency reasons. + */ + public T unique() { + return build().unique(); + } + + /** + * Shorthand for {@link QueryBuilder#build() build()}.{@link Query#uniqueOrThrow() uniqueOrThrow()}; see + * {@link Query#uniqueOrThrow()} for details. To execute a query more than once, you should build the query and + * keep + * the {@link Query} object for efficiency reasons. + */ + public T uniqueOrThrow() { + return build().uniqueOrThrow(); + } + + /** + * Shorthand for {@link QueryBuilder#buildCount() buildCount()}.{@link CountQuery#count() count()}; see + * {@link CountQuery#count()} for details. To execute a query more than once, you should build the query and keep + * the {@link CountQuery} object for efficiency reasons. + */ + public long count() { + return buildCount().count(); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/WhereCollector.java b/greenDao/src/main/java/org/greenrobot/greendao/query/WhereCollector.java new file mode 100644 index 0000000..3c8d597 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/WhereCollector.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.Property; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +/** Internal class to collect WHERE conditions. */ +class WhereCollector { + + private final AbstractDao dao; + private final List whereConditions; + private final String tablePrefix; + + WhereCollector(AbstractDao dao, String tablePrefix) { + this.dao = dao; + this.tablePrefix = tablePrefix; + whereConditions = new ArrayList(); + } + + void add(WhereCondition cond, WhereCondition... condMore) { + checkCondition(cond); + whereConditions.add(cond); + for (WhereCondition whereCondition : condMore) { + checkCondition(whereCondition); + whereConditions.add(whereCondition); + } + } + + WhereCondition combineWhereConditions(String combineOp, WhereCondition cond1, WhereCondition cond2, + WhereCondition... condMore) { + StringBuilder builder = new StringBuilder("("); + List combinedValues = new ArrayList(); + + addCondition(builder, combinedValues, cond1); + builder.append(combineOp); + addCondition(builder, combinedValues, cond2); + + for (WhereCondition cond : condMore) { + builder.append(combineOp); + addCondition(builder, combinedValues, cond); + } + builder.append(')'); + return new WhereCondition.StringCondition(builder.toString(), combinedValues.toArray()); + } + + void addCondition(StringBuilder builder, List values, WhereCondition condition) { + checkCondition(condition); + condition.appendTo(builder, tablePrefix); + condition.appendValuesTo(values); + } + + void checkCondition(WhereCondition whereCondition) { + if (whereCondition instanceof WhereCondition.PropertyCondition) { + checkProperty(((WhereCondition.PropertyCondition) whereCondition).property); + } + } + + void checkProperty(Property property) { + if (dao != null) { + Property[] properties = dao.getProperties(); + boolean found = false; + for (Property property2 : properties) { + if (property == property2) { + found = true; + break; + } + } + if (!found) { + throw new DaoException("Property '" + property.name + "' is not part of " + dao); + } + } + } + + void appendWhereClause(StringBuilder builder, String tablePrefixOrNull, List values) { + ListIterator iter = whereConditions.listIterator(); + while (iter.hasNext()) { + if (iter.hasPrevious()) { + builder.append(" AND "); + } + WhereCondition condition = iter.next(); + condition.appendTo(builder, tablePrefixOrNull); + condition.appendValuesTo(values); + } + } + + boolean isEmpty() { + return whereConditions.isEmpty(); + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/query/WhereCondition.java b/greenDao/src/main/java/org/greenrobot/greendao/query/WhereCondition.java new file mode 100644 index 0000000..058fa18 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/query/WhereCondition.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.query; + +import java.util.Date; +import java.util.List; + +import org.greenrobot.greendao.DaoException; +import org.greenrobot.greendao.Property; +import org.greenrobot.greendao.internal.SqlUtils; + +/** + * Internal interface to model WHERE conditions used in queries. Use the {@link Property} objects in the DAO classes to + * create new conditions. + */ +public interface WhereCondition { + + void appendTo(StringBuilder builder, String tableAlias); + + void appendValuesTo(List values); + + abstract class AbstractCondition implements WhereCondition { + + protected final boolean hasSingleValue; + protected final Object value; + protected final Object[] values; + + public AbstractCondition() { + hasSingleValue = false; + value = null; + values = null; + } + + public AbstractCondition(Object value) { + this.value = value; + hasSingleValue = true; + values = null; + } + + public AbstractCondition(Object[] values) { + this.value = null; + hasSingleValue = false; + this.values = values; + } + + @Override + public void appendValuesTo(List valuesTarget) { + if (hasSingleValue) { + valuesTarget.add(value); + } else if (values != null) { + for (Object value : values) { + valuesTarget.add(value); + } + } + } + } + + class PropertyCondition extends AbstractCondition { + + private static Object checkValueForType(Property property, Object value) { + if (value != null && value.getClass().isArray()) { + throw new DaoException("Illegal value: found array, but simple object required"); + } + Class type = property.type; + if (type == Date.class) { + if (value instanceof Date) { + return ((Date) value).getTime(); + } else if (value instanceof Long) { + return value; + } else { + throw new DaoException("Illegal date value: expected java.util.Date or Long for value " + value); + } + } else if (property.type == boolean.class || property.type == Boolean.class) { + if (value instanceof Boolean) { + return ((Boolean) value) ? 1 : 0; + } else if (value instanceof Number) { + int intValue = ((Number) value).intValue(); + if (intValue != 0 && intValue != 1) { + throw new DaoException("Illegal boolean value: numbers must be 0 or 1, but was " + value); + } + } else if (value instanceof String) { + String stringValue = ((String) value); + if ("TRUE".equalsIgnoreCase(stringValue)) { + return 1; + } else if ("FALSE".equalsIgnoreCase(stringValue)) { + return 0; + } else { + throw new DaoException( + "Illegal boolean value: Strings must be \"TRUE\" or \"FALSE\" (case insensitive), but was " + + value); + } + } + } + return value; + } + + private static Object[] checkValuesForType(Property property, Object[] values) { + for (int i = 0; i < values.length; i++) { + values[i] = checkValueForType(property, values[i]); + } + return values; + } + + public final Property property; + public final String op; + + public PropertyCondition(Property property, String op) { + this.property = property; + this.op = op; + } + + public PropertyCondition(Property property, String op, Object value) { + super(checkValueForType(property, value)); + this.property = property; + this.op = op; + } + + public PropertyCondition(Property property, String op, Object[] values) { + super(checkValuesForType(property, values)); + this.property = property; + this.op = op; + } + + @Override + public void appendTo(StringBuilder builder, String tableAlias) { + SqlUtils.appendProperty(builder, tableAlias, property).append(op); + } + } + + class StringCondition extends AbstractCondition { + + protected final String string; + + public StringCondition(String string) { + this.string = string; + } + + public StringCondition(String string, Object value) { + super(value); + this.string = string; + } + + public StringCondition(String string, Object... values) { + super(values); + this.string = string; + } + + @Override + public void appendTo(StringBuilder builder, String tableAlias) { + builder.append(string); + } + + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/rx/RxBase.java b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxBase.java new file mode 100644 index 0000000..a4b6baf --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxBase.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.rx; + + +import org.greenrobot.greendao.annotation.apihint.Internal; + +import java.util.concurrent.Callable; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.annotations.Experimental; + +/** + * Base functionality for Rx, e.g. default scheduler. + */ +@Internal +class RxBase { + + protected final Scheduler scheduler; + + /** + * No default scheduler. + */ + RxBase() { + scheduler = null; + } + + /** + * Sets the default scheduler, which is used to configure returned observables with + * {@link Observable#subscribeOn(Scheduler)}. + */ + @Experimental + RxBase(Scheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * The default scheduler (or null) used for wrapping. + */ + @Experimental + public Scheduler getScheduler() { + return scheduler; + } + + protected Observable wrap(Callable callable) { + return wrap(RxUtils.fromCallable(callable)); + } + + protected Observable wrap(Observable observable) { + if (scheduler != null) { + return observable.subscribeOn(scheduler); + } else { + return observable; + } + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/rx/RxDao.java b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxDao.java new file mode 100644 index 0000000..8bc3956 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxDao.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.rx; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.annotation.apihint.Experimental; + +import java.util.List; +import java.util.concurrent.Callable; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; + + +/** + * Like {@link AbstractDao} but with Rx support. Most methods from AbstractDao are present here, but will return an + * {@link Observable}. Modifying operations return the given entities, so they can be further processed in Rx. + *

    + * + * Note: DO NOT call more than one data modification operation when you can use a transaction instead (see + * + */ +@Experimental +public class RxDao extends RxBase { + + private final AbstractDao dao; + + /** + * Creates a new RxDao without a default scheduler. + */ + @Experimental + public RxDao(AbstractDao dao) { + this(dao, null); + } + + /** + * Creates a new RxDao with a default scheduler, which is used to configure returned observables with + * {@link Observable#subscribeOn(Scheduler)}. + */ + @Experimental + public RxDao(AbstractDao dao, Scheduler scheduler) { + super(scheduler); + this.dao = dao; + } + + /** + * Rx version of {@link AbstractDao#loadAll()} returning an Observable. + */ + @Experimental + public Observable> loadAll() { + return wrap(new Callable>() { + @Override + public List call() throws Exception { + return dao.loadAll(); + } + }); + } + + /** + * Rx version of {@link AbstractDao#loadAll()} returning an Observable. + */ + @Experimental + public Observable load(final K key) { + return wrap(new Callable() { + @Override + public T call() throws Exception { + return dao.load(key); + } + }); + } + + /** + * Rx version of {@link AbstractDao#refresh(Object)} returning an Observable. + * Note that the Observable will emit the given entity back to its subscribers. + */ + @Experimental + public Observable refresh(final T entity) { + return wrap(new Callable() { + @Override + public T call() throws Exception { + dao.refresh(entity); + return entity; + } + }); + } + + /** + * Rx version of {@link AbstractDao#insert(Object)} returning an Observable. + * Note that the Observable will emit the given entity back to its subscribers. + */ + @Experimental + public Observable insert(final T entity) { + return wrap(new Callable() { + @Override + public T call() throws Exception { + dao.insert(entity); + return entity; + } + }); + } + + /** + * Rx version of {@link AbstractDao#insertInTx(Iterable)} returning an Observable. + * Note that the Observable will emit the given entities back to its subscribers. + */ + @Experimental + public Observable> insertInTx(final Iterable entities) { + return wrap(new Callable>() { + @Override + public Iterable call() throws Exception { + dao.insertInTx(entities); + return entities; + } + }); + } + + /** + * Rx version of {@link AbstractDao#insertInTx(Object[])} returning an Observable. + * Note that the Observable will emit the given entities back to its subscribers. + */ + @Experimental + public Observable insertInTx(final T... entities) { + return wrap(new Callable() { + @Override + public Object[] call() throws Exception { + dao.insertInTx(entities); + return entities; + } + }); + } + + /** + * Rx version of {@link AbstractDao#insertOrReplace(Object)} returning an Observable. + * Note that the Observable will emit the given entity back to its subscribers. + */ + @Experimental + public Observable insertOrReplace(final T entity) { + return wrap(new Callable() { + @Override + public T call() throws Exception { + dao.insertOrReplace(entity); + return entity; + } + }); + } + + /** + * Rx version of {@link AbstractDao#insertOrReplaceInTx(Iterable)} returning an Observable. + * Note that the Observable will emit the given entities back to its subscribers. + */ + @Experimental + public Observable> insertOrReplaceInTx(final Iterable entities) { + return wrap(new Callable>() { + @Override + public Iterable call() throws Exception { + dao.insertOrReplaceInTx(entities); + return entities; + } + }); + } + + /** + * Rx version of {@link AbstractDao#insertOrReplaceInTx(Object[])} returning an Observable. + * Note that the Observable will emit the given entities back to its subscribers. + */ + @Experimental + public Observable insertOrReplaceInTx(final T... entities) { + return wrap(new Callable() { + @Override + public Object[] call() throws Exception { + dao.insertOrReplaceInTx(entities); + return entities; + } + }); + } + + /** + * Rx version of {@link AbstractDao#save(Object)} returning an Observable. + * Note that the Observable will emit the given entity back to its subscribers. + */ + @Experimental + public Observable save(final T entity) { + return wrap(new Callable() { + @Override + public T call() throws Exception { + dao.save(entity); + return entity; + } + }); + } + + /** + * Rx version of {@link AbstractDao#saveInTx(Iterable)} returning an Observable. + * Note that the Observable will emit the given entities back to its subscribers. + */ + @Experimental + public Observable> saveInTx(final Iterable entities) { + return wrap(new Callable>() { + @Override + public Iterable call() throws Exception { + dao.saveInTx(entities); + return entities; + } + }); + } + + /** + * Rx version of {@link AbstractDao#saveInTx(Object[])} returning an Observable. + * Note that the Observable will emit the given entities back to its subscribers. + */ + @Experimental + public Observable saveInTx(final T... entities) { + return wrap(new Callable() { + @Override + public Object[] call() throws Exception { + dao.saveInTx(entities); + return entities; + } + }); + } + + /** + * Rx version of {@link AbstractDao#update(Object)} returning an Observable. + * Note that the Observable will emit the given entity back to its subscribers. + */ + @Experimental + public Observable update(final T entity) { + return wrap(new Callable() { + @Override + public T call() throws Exception { + dao.update(entity); + return entity; + } + }); + } + + /** + * Rx version of {@link AbstractDao#updateInTx(Iterable)} returning an Observable. + * Note that the Observable will emit the given entities back to its subscribers. + */ + @Experimental + public Observable> updateInTx(final Iterable entities) { + return wrap(new Callable>() { + @Override + public Iterable call() throws Exception { + dao.updateInTx(entities); + return entities; + } + }); + } + + /** + * Rx version of {@link AbstractDao#updateInTx(Object[])} returning an Observable. + * Note that the Observable will emit the given entities back to its subscribers. + */ + @Experimental + public Observable updateInTx(final T... entities) { + return wrap(new Callable() { + @Override + public Object[] call() throws Exception { + dao.updateInTx(entities); + return entities; + } + }); + } + + + /** + * Rx version of {@link AbstractDao#delete(Object)} returning an Observable. + */ + @Experimental + public Observable delete(final T entity) { + return wrap(new Callable() { + @Override + public Void call() throws Exception { + dao.delete(entity); + return null; + } + }); + } + + /** + * Rx version of {@link AbstractDao#deleteByKey(Object)} returning an Observable. + */ + @Experimental + public Observable deleteByKey(final K key) { + return wrap(new Callable() { + @Override + public Void call() throws Exception { + dao.deleteByKey(key); + return null; + } + }); + } + + /** + * Rx version of {@link AbstractDao#deleteAll()} returning an Observable. + */ + @Experimental + public Observable deleteAll() { + return wrap(new Callable() { + @Override + public Void call() throws Exception { + dao.deleteAll(); + return null; + } + }); + } + + /** + * Rx version of {@link AbstractDao#deleteInTx(Iterable)} returning an Observable. + */ + @Experimental + public Observable deleteInTx(final Iterable entities) { + return wrap(new Callable() { + @Override + public Void call() throws Exception { + dao.deleteInTx(entities); + return null; + } + }); + } + + /** + * Rx version of {@link AbstractDao#deleteInTx(Object[])} returning an Observable. + */ + @Experimental + public Observable deleteInTx(final T... entities) { + return wrap(new Callable() { + @Override + public Void call() throws Exception { + dao.deleteInTx(entities); + return null; + } + }); + } + + /** + * Rx version of {@link AbstractDao#deleteByKeyInTx(Iterable)} returning an Observable. + */ + @Experimental + public Observable deleteByKeyInTx(final Iterable keys) { + return wrap(new Callable() { + @Override + public Void call() throws Exception { + dao.deleteByKeyInTx(keys); + return null; + } + }); + } + + /** + * Rx version of {@link AbstractDao#deleteByKeyInTx(Object[])} returning an Observable. + */ + @Experimental + public Observable deleteByKeyInTx(final K... keys) { + return wrap(new Callable() { + @Override + public Void call() throws Exception { + dao.deleteByKeyInTx(keys); + return null; + } + }); + } + + /** + * Rx version of {@link AbstractDao#count()} returning an Observable. + */ + @Experimental + public Observable count() { + return wrap(new Callable() { + @Override + public Long call() throws Exception { + return dao.count(); + } + }); + } + + /** + * The plain DAO. + */ + @Experimental + public AbstractDao getDao() { + return dao; + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/rx/RxQuery.java b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxQuery.java new file mode 100644 index 0000000..15f2e18 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxQuery.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.rx; + +import org.greenrobot.greendao.query.LazyList; +import org.greenrobot.greendao.query.Query; +import org.reactivestreams.Subscriber; + +import java.util.List; +import java.util.concurrent.Callable; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.annotations.Experimental; +import io.reactivex.exceptions.Exceptions; + +/** + * Gets {@link org.greenrobot.greendao.query.Query} results in Rx fashion. + */ +@Experimental +// TODO Pass parameters: currently, parameters are always set to their initial values because of forCurrentThread() +public class RxQuery extends RxBase { + private final Query query; + + public RxQuery(Query query) { + this.query = query; + } + + public RxQuery(Query query, Scheduler scheduler) { + super(scheduler); + this.query = query; + } + + /** + * Rx version of {@link Query#list()} returning an Observable. + */ + @Experimental + public Observable> list() { + return wrap(new Callable>() { + @Override + public List call() throws Exception { + return query.forCurrentThread().list(); + } + }); + } + + /** + * Rx version of {@link Query#unique()} returning an Observable. + */ + @Experimental + public Observable unique() { + return wrap(new Callable() { + @Override + public T call() throws Exception { + return query.forCurrentThread().unique(); + } + }); + } + + /** + * Emits the resulting entities one by one, producing them on the fly ("streaming" entities). + * Unlike {@link #list()}, it does not wait for the query to gather all results. Thus, the first entities are + * immediately available as soon the underlying database cursor has data. This approach may be more memory + * efficient for large number of entities (or large entities) at the cost of additional overhead caused by a + * per-entity delivery through Rx. + */ +// public Observable oneByOne() { +// Observable observable = Observable.create(new OnSubscribe() { +// @Override +// public void call(Subscriber subscriber) { +// try { +// LazyList lazyList = query.forCurrentThread().listLazyUncached(); +// try { +// for (T entity : lazyList) { +// if (subscriber.isUnsubscribed()) { +// break; +// } +// subscriber.onNext(entity); +// } +// } finally { +// lazyList.close(); +// } +// if (!subscriber.isUnsubscribed()) { +// subscriber.onCompleted(); +// } +// } catch (Throwable e) { +// Exceptions.throwIfFatal(e); +// subscriber.onError(e); +// } +// } +// }); +// return wrap(observable); +// } + +// @Experimental +// public Query getQuery() { +// return query; +// } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/rx/RxTransaction.java b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxTransaction.java new file mode 100644 index 0000000..145ca14 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxTransaction.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.rx; + +import org.greenrobot.greendao.AbstractDaoSession; +import org.greenrobot.greendao.annotation.apihint.Experimental; + +import java.util.concurrent.Callable; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; + + +/** + * Allows to do transactions using Rx Observable. + */ +@Experimental +public class RxTransaction extends RxBase { + private final AbstractDaoSession daoSession; + + public RxTransaction(AbstractDaoSession daoSession) { + this.daoSession = daoSession; + } + + public RxTransaction(AbstractDaoSession daoSession, Scheduler scheduler) { + super(scheduler); + this.daoSession = daoSession; + } + + /** + * Rx version of {@link AbstractDaoSession#runInTx(Runnable)} returning an Observable. + */ + @Experimental + public Observable run(final Runnable runnable) { + return wrap(new Callable() { + @Override + public Void call() throws Exception { + daoSession.runInTx(runnable); + return null; + } + }); + } + + /** + * Rx version of {@link AbstractDaoSession#callInTx(Callable)} returning an Observable. + */ + @Experimental + public Observable call(final Callable callable) { + return wrap(new Callable() { + @Override + public T call() throws Exception { + return daoSession.callInTx(callable); + } + }); + } + + // Note: wrapping callInTxNoException does not make sense, because the Exception is handled by Rx anyway. + + + @Experimental + public AbstractDaoSession getDaoSession() { + return daoSession; + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/rx/RxUtils.java b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxUtils.java new file mode 100644 index 0000000..240f1e6 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/rx/RxUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.rx; + +import org.greenrobot.greendao.annotation.apihint.Internal; + +import java.util.concurrent.Callable; + + +import java.util.concurrent.Callable; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; + + +@Internal +class RxUtils { + /** As of RxJava 1.1.7, Observable.fromCallable is still @Beta, so just in case... */ + @Internal + static Observable fromCallable(final Callable callable) { +// return Observable.defer(new Func0>() { +// +// @Override +// public Observable call() { +// T result; +// try { +// result = callable.call(); +// } catch (Exception e) { +// return Observable.error(e); +// } +// return Observable.just(result); +// } +// }); + return Observable.defer(new Callable>() { + @Override + public ObservableSource call() throws Exception { + T result; + try { + result = callable.call(); + } catch (Exception e) { + return Observable.error(e); + } + return Observable.just(result); + } + }); + } +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoSessionTest.java b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoSessionTest.java new file mode 100644 index 0000000..cc5aa04 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoSessionTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import android.database.sqlite.SQLiteDatabase; +import org.greenrobot.greendao.AbstractDaoMaster; +import org.greenrobot.greendao.AbstractDaoSession; +import org.greenrobot.greendao.database.Database; + +/** + * Base class for DAO (master) related testing. + * + * @author Markus + * + * @param + * Type of a concrete DAO master + */ +public abstract class AbstractDaoSessionTest + extends DbTest { + + private final Class daoMasterClass; + protected T daoMaster; + protected S daoSession; + + public AbstractDaoSessionTest(Class daoMasterClass) { + this(daoMasterClass, true); + } + + public AbstractDaoSessionTest(Class daoMasterClass, boolean inMemory) { + super(inMemory); + this.daoMasterClass = daoMasterClass; + } + + @SuppressWarnings("unchecked") + @Override + protected void setUp() throws Exception { + super.setUp(); + try { + Constructor constructor = daoMasterClass.getConstructor(Database.class); + daoMaster = constructor.newInstance(db); + + Method createTableMethod = daoMasterClass.getMethod("createAllTables", Database.class, boolean.class); + createTableMethod.invoke(null, db, false); + } catch (Exception e) { + throw new RuntimeException("Could not prepare DAO session test", e); + } + daoSession = (S) daoMaster.newSession(); + } + +} \ No newline at end of file diff --git a/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTest.java b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTest.java new file mode 100644 index 0000000..f96bccf --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.test; + +import java.lang.reflect.Method; + +import android.database.sqlite.SQLiteDatabase; +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoLog; +import org.greenrobot.greendao.InternalUnitTestDaoAccess; +import org.greenrobot.greendao.Property; +import org.greenrobot.greendao.database.Database; +import org.greenrobot.greendao.identityscope.IdentityScope; + +/** + * Base class for DAO related testing without any tests. Prepares an in-memory DB and DAO. + * + * @author Markus + * + * @param + * DAO class + * @param + * Entity type of the DAO + * @param + * Key type of the DAO + */ +public abstract class AbstractDaoTest, T, K> extends DbTest { + + protected final Class daoClass; + protected D dao; + protected InternalUnitTestDaoAccess daoAccess; + protected Property pkColumn; + protected IdentityScope identityScopeForDao; + + public AbstractDaoTest(Class daoClass) { + this(daoClass, true); + } + + public AbstractDaoTest(Class daoClass, boolean inMemory) { + super(inMemory); + this.daoClass = daoClass; + } + + public void setIdentityScopeBeforeSetUp(IdentityScope identityScope) { + this.identityScopeForDao = identityScope; + } + + @SuppressWarnings("unchecked") + @Override + protected void setUp() throws Exception { + super.setUp(); + try { + setUpTableForDao(); + daoAccess = new InternalUnitTestDaoAccess(db, (Class>) daoClass, identityScopeForDao); + dao = (D) daoAccess.getDao(); + } catch (Exception e) { + throw new RuntimeException("Could not prepare DAO Test", e); + } + } + + protected void setUpTableForDao() throws Exception { + try { + Method createTableMethod = daoClass.getMethod("createTable", Database.class, boolean.class); + createTableMethod.invoke(null, db, false); + } catch (NoSuchMethodException e) { + DaoLog.i("No createTable method"); + } + } + + protected void clearIdentityScopeIfAny() { + if (identityScopeForDao != null) { + identityScopeForDao.clear(); + DaoLog.d("Identity scope cleared"); + } else { + DaoLog.d("No identity scope to clear"); + } + } + + protected void logTableDump() { + logTableDump(dao.getTablename()); + } +} \ No newline at end of file diff --git a/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestLongPk.java b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestLongPk.java new file mode 100644 index 0000000..df6597e --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestLongPk.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.test; + +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoLog; + +/** + * Base class for DAOs having a long/Long as a PK, which is quite common. + * + * @param DAO class + * @param Entity type of the DAO + * @author Markus + */ +public abstract class AbstractDaoTestLongPk, T> extends AbstractDaoTestSinglePk { + + public AbstractDaoTestLongPk(Class daoClass) { + super(daoClass); + } + + /** {@inheritDoc} */ + protected Long createRandomPk() { + return random.nextLong(); + } + + public void testAssignPk() { + if (daoAccess.isEntityUpdateable()) { + T entity1 = createEntity(null); + if (entity1 != null) { + T entity2 = createEntity(null); + + dao.insert(entity1); + dao.insert(entity2); + + Long pk1 = daoAccess.getKey(entity1); + assertNotNull(pk1); + Long pk2 = daoAccess.getKey(entity2); + assertNotNull(pk2); + + assertFalse(pk1.equals(pk2)); + + assertNotNull(dao.load(pk1)); + assertNotNull(dao.load(pk2)); + } else { + DaoLog.d("Skipping testAssignPk for " + daoClass + " (createEntity returned null for null key)"); + } + } else { + DaoLog.d("Skipping testAssignPk for not updateable " + daoClass); + } + } + +} \ No newline at end of file diff --git a/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestSinglePk.java b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestSinglePk.java new file mode 100644 index 0000000..2e69901 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestSinglePk.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.test; + +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.SQLException; +import org.greenrobot.greendao.AbstractDao; +import org.greenrobot.greendao.DaoLog; +import org.greenrobot.greendao.Property; +import org.greenrobot.greendao.internal.SqlUtils; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Default tests for single-PK entities. + * + * @param DAO class + * @param Entity type of the DAO + * @param Key type of the DAO + * @author Markus + */ +public abstract class AbstractDaoTestSinglePk, T, K> extends AbstractDaoTest { + + protected Set usedPks; + private Property pkColumn; + + public AbstractDaoTestSinglePk(Class daoClass) { + super(daoClass); + usedPks = new HashSet(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + Property[] columns = daoAccess.getProperties(); + for (Property column : columns) { + if (column.primaryKey) { + if (pkColumn != null) { + throw new RuntimeException("Test does not work with multiple PK columns"); + } + pkColumn = column; + } + } + if (pkColumn == null) { + throw new RuntimeException("Test does not work without a PK column"); + } + } + + public void testInsertAndLoad() { + K pk = nextPk(); + T entity = createEntity(pk); + dao.insert(entity); + assertEquals(pk, daoAccess.getKey(entity)); + T entity2 = dao.load(pk); + assertNotNull(entity2); + assertEquals(daoAccess.getKey(entity), daoAccess.getKey(entity2)); + } + + public void testInsertInTx() { + dao.deleteAll(); + List list = new ArrayList(); + for (int i = 0; i < 20; i++) { + list.add(createEntityWithRandomPk()); + } + dao.insertInTx(list); + assertEquals(list.size(), dao.count()); + } + + public void testCount() { + dao.deleteAll(); + assertEquals(0, dao.count()); + dao.insert(createEntityWithRandomPk()); + assertEquals(1, dao.count()); + dao.insert(createEntityWithRandomPk()); + assertEquals(2, dao.count()); + } + + public void testInsertTwice() { + K pk = nextPk(); + T entity = createEntity(pk); + dao.insert(entity); + try { + dao.insert(entity); + fail("Inserting twice should not work"); + } catch (SQLException expected) { + // OK + } + } + + public void testInsertOrReplaceTwice() { + T entity = createEntityWithRandomPk(); + long rowId1 = dao.insert(entity); + long rowId2 = dao.insertOrReplace(entity); + if (dao.getPkProperty().type == Long.class) { + assertEquals(rowId1, rowId2); + } + } + + public void testInsertOrReplaceInTx() { + dao.deleteAll(); + List listPartial = new ArrayList(); + List listAll = new ArrayList(); + for (int i = 0; i < 20; i++) { + T entity = createEntityWithRandomPk(); + if (i % 2 == 0) { + listPartial.add(entity); + } + listAll.add(entity); + } + dao.insertOrReplaceInTx(listPartial); + dao.insertOrReplaceInTx(listAll); + assertEquals(listAll.size(), dao.count()); + } + + public void testDelete() { + K pk = nextPk(); + dao.deleteByKey(pk); + T entity = createEntity(pk); + dao.insert(entity); + assertNotNull(dao.load(pk)); + dao.deleteByKey(pk); + assertNull(dao.load(pk)); + } + + public void testDeleteAll() { + List entityList = new ArrayList(); + for (int i = 0; i < 10; i++) { + T entity = createEntityWithRandomPk(); + entityList.add(entity); + } + dao.insertInTx(entityList); + dao.deleteAll(); + assertEquals(0, dao.count()); + for (T entity : entityList) { + K key = daoAccess.getKey(entity); + assertNotNull(key); + assertNull(dao.load(key)); + } + } + + public void testDeleteInTx() { + List entityList = new ArrayList(); + for (int i = 0; i < 10; i++) { + T entity = createEntityWithRandomPk(); + entityList.add(entity); + } + dao.insertInTx(entityList); + List entitiesToDelete = new ArrayList(); + entitiesToDelete.add(entityList.get(0)); + entitiesToDelete.add(entityList.get(3)); + entitiesToDelete.add(entityList.get(4)); + entitiesToDelete.add(entityList.get(8)); + dao.deleteInTx(entitiesToDelete); + assertEquals(entityList.size() - entitiesToDelete.size(), dao.count()); + for (T deletedEntity : entitiesToDelete) { + K key = daoAccess.getKey(deletedEntity); + assertNotNull(key); + assertNull(dao.load(key)); + } + } + + public void testDeleteByKeyInTx() { + List entityList = new ArrayList(); + for (int i = 0; i < 10; i++) { + T entity = createEntityWithRandomPk(); + entityList.add(entity); + } + dao.insertInTx(entityList); + List keysToDelete = new ArrayList(); + keysToDelete.add(daoAccess.getKey(entityList.get(0))); + keysToDelete.add(daoAccess.getKey(entityList.get(3))); + keysToDelete.add(daoAccess.getKey(entityList.get(4))); + keysToDelete.add(daoAccess.getKey(entityList.get(8))); + dao.deleteByKeyInTx(keysToDelete); + assertEquals(entityList.size() - keysToDelete.size(), dao.count()); + for (K key : keysToDelete) { + assertNotNull(key); + assertNull(dao.load(key)); + } + } + + public void testRowId() { + T entity1 = createEntityWithRandomPk(); + T entity2 = createEntityWithRandomPk(); + long rowId1 = dao.insert(entity1); + long rowId2 = dao.insert(entity2); + assertTrue(rowId1 != rowId2); + } + + public void testLoadAll() { + dao.deleteAll(); + List list = new ArrayList(); + for (int i = 0; i < 15; i++) { + T entity = createEntity(nextPk()); + list.add(entity); + } + dao.insertInTx(list); + List loaded = dao.loadAll(); + assertEquals(list.size(), loaded.size()); + } + + public void testQuery() { + dao.insert(createEntityWithRandomPk()); + K pkForQuery = nextPk(); + dao.insert(createEntity(pkForQuery)); + dao.insert(createEntityWithRandomPk()); + + String where = "WHERE " + dao.getPkColumns()[0] + "=?"; + List list = dao.queryRaw(where, pkForQuery.toString()); + assertEquals(1, list.size()); + assertEquals(pkForQuery, daoAccess.getKey(list.get(0))); + } + + public void testUpdate() { + dao.deleteAll(); + T entity = createEntityWithRandomPk(); + dao.insert(entity); + dao.update(entity); + assertEquals(1, dao.count()); + } + + public void testReadWithOffset() { + K pk = nextPk(); + T entity = createEntity(pk); + dao.insert(entity); + + Cursor cursor = queryWithDummyColumnsInFront(5, "42", pk); + try { + T entity2 = daoAccess.readEntity(cursor, 5); + assertEquals(pk, daoAccess.getKey(entity2)); + } finally { + cursor.close(); + } + } + + public void testLoadPkWithOffset() { + runLoadPkTest(10); + } + + public void testLoadPk() { + runLoadPkTest(0); + } + + public void testSave() { + if(!checkKeyIsNullable()) { + return; + } + dao.deleteAll(); + T entity = createEntity(null); + if (entity != null) { + dao.save(entity); + dao.save(entity); + assertEquals(1, dao.count()); + } + } + + public void testSaveInTx() { + if(!checkKeyIsNullable()) { + return; + } + dao.deleteAll(); + List listPartial = new ArrayList(); + List listAll = new ArrayList(); + for (int i = 0; i < 20; i++) { + T entity = createEntity(null); + if (i % 2 == 0) { + listPartial.add(entity); + } + listAll.add(entity); + } + dao.saveInTx(listPartial); + dao.saveInTx(listAll); + assertEquals(listAll.size(), dao.count()); + } + + protected void runLoadPkTest(int offset) { + K pk = nextPk(); + T entity = createEntity(pk); + dao.insert(entity); + + Cursor cursor = queryWithDummyColumnsInFront(offset, "42", pk); + try { + K pk2 = daoAccess.readKey(cursor, offset); + assertEquals(pk, pk2); + } finally { + cursor.close(); + } + } + + protected Cursor queryWithDummyColumnsInFront(int dummyCount, String valueForColumn, K pk) { + StringBuilder builder = new StringBuilder("SELECT "); + for (int i = 0; i < dummyCount; i++) { + builder.append(valueForColumn).append(","); + } + SqlUtils.appendColumns(builder, "T", dao.getAllColumns()).append(" FROM "); + builder.append('"').append(dao.getTablename()).append('"').append(" T"); + if (pk != null) { + builder.append(" WHERE "); + + assertEquals(1, dao.getPkColumns().length); + builder.append(dao.getPkColumns()[0]).append("="); + DatabaseUtils.appendValueToSql(builder, pk); + } + + String select = builder.toString(); + Cursor cursor = db.rawQuery(select, null); + assertTrue(cursor.moveToFirst()); + try { + for (int i = 0; i < dummyCount; i++) { + assertEquals(valueForColumn, cursor.getString(i)); + } + if (pk != null) { + assertEquals(1, cursor.getCount()); + } + } catch (RuntimeException ex) { + cursor.close(); + throw ex; + } + return cursor; + } + + protected boolean checkKeyIsNullable() { + if (createEntity(null) == null) { + DaoLog.d("Test is not available for entities with non-null keys"); + return false; + } + return true; + } + + /** Provides a collision free PK () not returned before in the current test. */ + protected K nextPk() { + for (int i = 0; i < 100000; i++) { + K pk = createRandomPk(); + if (usedPks.add(pk)) { + return pk; + } + } + throw new IllegalStateException("Could not find a new PK"); + } + + protected T createEntityWithRandomPk() { + return createEntity(nextPk()); + } + + /** K does not have to be collision free, check nextPk for collision free PKs. */ + protected abstract K createRandomPk(); + + /** + * Creates an insertable entity. If the given key is null, but the entity's PK is not null the method must return + * null. + */ + protected abstract T createEntity(K key); + +} \ No newline at end of file diff --git a/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestStringPk.java b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestStringPk.java new file mode 100644 index 0000000..7584c91 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/test/AbstractDaoTestStringPk.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.greenrobot.greendao.test; + +import org.greenrobot.greendao.AbstractDao; + +/** + * Base class for DAOs having a String as a PK. + * + * @author Markus + * + * @param + * DAO class + * @param + * Entity type of the DAO + */ +public abstract class AbstractDaoTestStringPk, T> extends + AbstractDaoTestSinglePk { + + public AbstractDaoTestStringPk(Class daoClass) { + super(daoClass); + } + + @Override + protected String createRandomPk() { + int len = 1 + random.nextInt(30); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < len; i++) { + char c = (char) ('a' + random.nextInt('z' - 'a')); + builder.append(c); + } + return builder.toString(); + } + +} diff --git a/greenDao/src/main/java/org/greenrobot/greendao/test/DbTest.java b/greenDao/src/main/java/org/greenrobot/greendao/test/DbTest.java new file mode 100644 index 0000000..d0af3f4 --- /dev/null +++ b/greenDao/src/main/java/org/greenrobot/greendao/test/DbTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.greenrobot.greendao.test; + +import android.app.Application; +import android.app.Instrumentation; +import android.database.sqlite.SQLiteDatabase; +import android.test.AndroidTestCase; +import org.greenrobot.greendao.DaoLog; +import org.greenrobot.greendao.DbUtils; +import org.greenrobot.greendao.database.StandardDatabase; +import org.greenrobot.greendao.database.Database; + +import java.util.Random; + +/** + * Base class for database related testing, which prepares an in-memory or an file-based DB (using the test {@link + * android.content.Context}). Also, offers some convenience methods to create new {@link Application} objects similar + * to {@link android.test.ApplicationTestCase}. + *

    + * Unlike ApplicationTestCase, this class should behave more correctly when you call {@link #createApplication(Class)} + * during {@link #setUp()}: {@link android.test.ApplicationTestCase#testApplicationTestCaseSetUpProperly()} leaves + * Application objects un-terminated. + * + * @author Markus + */ +public abstract class DbTest extends AndroidTestCase { + + public static final String DB_NAME = "greendao-unittest-db.temp"; + + protected final Random random; + protected final boolean inMemory; + protected Database db; + + private Application application; + + public DbTest() { + this(true); + } + + public DbTest(boolean inMemory) { + this.inMemory = inMemory; + random = new Random(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + db = createDatabase(); + } + + /** Returns a prepared application with the onCreate method already called. */ + public T createApplication(Class appClass) { + assertNull("Application already created", application); + T app; + try { + app = (T) Instrumentation.newApplication(appClass, getContext()); + } catch (Exception e) { + throw new RuntimeException("Could not create application " + appClass, e); + } + app.onCreate(); + application = app; + return app; + } + + /** Terminates a previously created application. Also called by {@link #tearDown()} if needed. */ + public void terminateApplication() { + assertNotNull("Application not yet created", application); + application.onTerminate(); + application = null; + } + + /** Gets the previously created application. */ + public T getApplication() { + assertNotNull("Application not yet created", application); + return (T) application; + } + + /** May be overriden by sub classes to set up a different db. */ + protected Database createDatabase() { + SQLiteDatabase sqLiteDatabase; + if (inMemory) { + sqLiteDatabase = SQLiteDatabase.create(null); + + } else { + getContext().deleteDatabase(DB_NAME); + sqLiteDatabase = getContext().openOrCreateDatabase(DB_NAME, 0, null); + } + return new StandardDatabase(sqLiteDatabase); + } + + @Override + /** Closes the db, and terminates an application, if one was created before. */ + protected void tearDown() throws Exception { + if (application != null) { + terminateApplication(); + } + db.close(); + if (!inMemory) { + getContext().deleteDatabase(DB_NAME); + } + super.tearDown(); + } + + protected void logTableDump(String tablename) { + if (db instanceof StandardDatabase) { + DbUtils.logTableDump(((StandardDatabase) db).getSQLiteDatabase(), tablename); + } else { + DaoLog.w("Table dump unsupported for " + db); + } + } + +} \ No newline at end of file diff --git a/greenDao/src/main/res/values/strings.xml b/greenDao/src/main/res/values/strings.xml new file mode 100644 index 0000000..e3a7072 --- /dev/null +++ b/greenDao/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + My Library + diff --git a/greenDao/src/test/java/org/greenrobot/greendao/ExampleUnitTest.java b/greenDao/src/test/java/org/greenrobot/greendao/ExampleUnitTest.java new file mode 100644 index 0000000..a2c8c5a --- /dev/null +++ b/greenDao/src/test/java/org/greenrobot/greendao/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package org.greenrobot.greendao; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..adcd3f0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':greenDao'