diff --git a/composite-builds/external/logback-android b/composite-builds/external/logback-android new file mode 160000 index 000000000..d108db548 --- /dev/null +++ b/composite-builds/external/logback-android @@ -0,0 +1 @@ +Subproject commit d108db548b51a5c36694247cd31c3a8ff1cfca47 diff --git a/lsp/java/build.gradle.kts b/lsp/java/build.gradle.kts index a90023832..b297a1119 100644 --- a/lsp/java/build.gradle.kts +++ b/lsp/java/build.gradle.kts @@ -18,60 +18,61 @@ import com.itsaky.androidide.build.config.BuildConfig plugins { - id("com.android.library") - id("kotlin-android") - id("kotlin-kapt") - id("realm-android") + id("com.android.library") + id("kotlin-android") + id("kotlin-kapt") + id("realm-android") } android { - namespace = "${BuildConfig.packageName}.lsp.java" + namespace = "${BuildConfig.packageName}.lsp.java" - sourceSets { - getByName("androidTest") { - assets.srcDirs(rootProject.file("subprojects/framework-stubs/libs")) - } + sourceSets { + getByName("androidTest") { + assets.srcDirs(rootProject.file("subprojects/framework-stubs/libs")) } + } } kapt { - arguments { - arg ("eventBusIndex", "${BuildConfig.packageName}.events.LspJavaEventsIndex") - } + arguments { + arg("eventBusIndex", "${BuildConfig.packageName}.events.LspJavaEventsIndex") + } } dependencies { - - kapt(projects.annotationProcessors) - - implementation(libs.androidide.ts) - implementation(libs.androidide.ts.java) - implementation(libs.common.editor) - implementation(libs.common.javaparser) - implementation(libs.common.utilcode) - implementation(libs.androidx.annotation) - implementation(libs.google.guava) - implementation(libs.google.gson) - - compileOnly(libs.androidx.appcompat) - compileOnly(libs.google.material) - compileOnly(projects.actions) - compileOnly(projects.common) - - implementation(projects.editorApi) - implementation(projects.resources) - implementation(projects.lsp.api) - implementation(projects.subprojects.indexing) - implementation(projects.subprojects.javacServices) + kapt(projects.annotationProcessors) - implementation(libs.composite.javac) - implementation(libs.composite.jdkJdeps) - implementation(libs.composite.javapoet) - implementation(libs.composite.googleJavaFormat) + implementation(libs.androidide.ts) + implementation(libs.androidide.ts.java) + implementation(libs.common.editor) + implementation(libs.common.javaparser) + implementation(libs.common.utilcode) + implementation(libs.androidx.annotation) + implementation(libs.google.guava) + implementation(libs.google.gson) - implementation(libs.androidx.core.ktx) - implementation(libs.common.kotlin) + compileOnly(libs.androidx.appcompat) + compileOnly(libs.google.material) + compileOnly(projects.actions) + compileOnly(projects.common) - testImplementation(projects.testing.lsp) - androidTestImplementation(projects.testing.android) + implementation(projects.editorApi) + implementation(projects.resources) + implementation(projects.lsp.api) + implementation(projects.subprojects.indexing) + implementation(projects.subprojects.javacServices) + + implementation(libs.composite.javac) + implementation(libs.composite.jdkJdeps) + implementation(libs.composite.javapoet) + implementation(libs.composite.googleJavaFormat) + + implementation(libs.androidx.core.ktx) + implementation(libs.common.kotlin) + + testImplementation(projects.testing.lsp) + androidTestImplementation(projects.testing.android) + androidTestImplementation(projects.common) + androidTestImplementation(projects.shared) } \ No newline at end of file diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/IndexingHelper.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/IndexingHelper.kt new file mode 100644 index 000000000..1c16d3ddb --- /dev/null +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/IndexingHelper.kt @@ -0,0 +1,72 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.java.indexing + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth +import com.itsaky.androidide.db.IRealmProvider +import com.itsaky.androidide.lsp.java.indexing.models.JavaIndexingRealmModule +import com.itsaky.androidide.utils.Environment +import io.realm.Realm +import io.realm.log.LogLevel +import io.realm.log.RealmLog +import java.io.File + +/** + * @author Akash Yadav + */ +class IndexingHelper( + private val dbName: String +) { + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + val androidJar: File by lazy { + val file = File.createTempFile("ajar", null, context.cacheDir) + context.assets.open("android.jar").use { asset -> + Truth.assertThat(asset).isNotNull() + file.outputStream().buffered().use { out -> + asset.copyTo(out) + out.flush() + } + } + file + } + + fun doWithRealm(deleteDbAfterAction: Boolean = true, action: Realm.() -> Unit) { + Realm.init(context.applicationContext) + RealmLog.setLevel(LogLevel.ALL) + + if (Environment.REALM_DB_DIR == null) { + Environment.init(context) + } + + val dbName = dbName.replace(IRealmProvider.PATH_SEPARATOR, '-') + val realm = IRealmProvider.instance().get("/indexing/java/$dbName") { + modules(JavaIndexingRealmModule()) + } + try { + realm.action() + } finally { + if (deleteDbAfterAction) { + realm.configuration.realmDirectory.deleteRecursively() + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaClassCRUDTest.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaClassCRUDTest.kt new file mode 100644 index 000000000..04888981b --- /dev/null +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaClassCRUDTest.kt @@ -0,0 +1,134 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.java.indexing + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.java.indexing.models.JavaClass +import com.itsaky.androidide.lsp.java.indexing.models.JavaField +import com.itsaky.androidide.lsp.java.indexing.models.JavaMethod +import com.itsaky.androidide.lsp.java.indexing.models.JavaType +import io.realm.RealmList +import io.realm.RealmObject +import openjdk.tools.classfile.AccessFlags +import org.junit.Test +import org.junit.runner.RunWith + +/** + * @author Akash Yadav + */ +@RunWith(AndroidJUnit4::class) +class JavaClassCRUDTest { + + @Test + fun testSimpleJavaClassCRUD() { + IndexingHelper("java-class-create").doWithRealm { + val klass = JavaClass.newInstance( + fqn = "com/itsaky/androidide/indexing/TestClass", + name = "TestClass", + packageName = "com/itsaky/androidide/indexing", + accessFlags = AccessFlags.ACC_PUBLIC or AccessFlags.ACC_FINAL, + superClassFqn = "java/lang/Object", + superInterfacesFqn = RealmList(), + fields = RealmList().apply { + add(JavaField.newField("someString", JavaType.STRING, AccessFlags.ACC_PRIVATE)) + }, + methods = RealmList().apply { + add( + JavaMethod.newInstance( + name = "getSomeString", + paramsTypes = RealmList(), + returnType = JavaType.STRING, + accessFlags = AccessFlags.ACC_PUBLIC + ) + ) + }) + + // CREATE + executeTransaction { + insert(klass) + } + + var dbKlass: JavaClass? = null + executeTransaction { + // READ + dbKlass = where(JavaClass::class.java) + .equalTo("fqn", "com/itsaky/androidide/indexing/TestClass") + .findFirst() + + assertThat(dbKlass).isNotNull() + assertThat(dbKlass?.fqn).isEqualTo("com/itsaky/androidide/indexing/TestClass") + assertThat(dbKlass?.name).isEqualTo("TestClass") + assertThat(dbKlass?.packageName).isEqualTo("com/itsaky/androidide/indexing") + assertThat(dbKlass?.accessFlags).isEqualTo(AccessFlags.ACC_PUBLIC or AccessFlags.ACC_FINAL) + assertThat(dbKlass?.superClassFqn).isEqualTo("java/lang/Object") + assertThat(dbKlass?.superInterfacesFqn).isEqualTo(RealmList()) + assertThat(dbKlass?.fields).isNotEmpty() + assertThat(dbKlass?.fields).hasSize(1) + + dbKlass?.fields?.get(0).apply { + assertThat(this).isNotNull() + assertThat(this?.name).isEqualTo("someString") + assertThat(this?.type).isEqualTo(JavaType.STRING) + assertThat(this?.accessFlags).isEqualTo(AccessFlags.ACC_PRIVATE) + } + + assertThat(dbKlass?.methods).isNotEmpty() + assertThat(dbKlass?.methods).hasSize(1) + + dbKlass?.methods?.get(0).apply { + assertThat(this).isNotNull() + assertThat(this?.name).isEqualTo("getSomeString") + assertThat(this?.paramsTypes).isEqualTo(RealmList()) + assertThat(this?.returnType).isEqualTo(JavaType.STRING) + assertThat(this?.accessFlags).isEqualTo(AccessFlags.ACC_PUBLIC) + } + } + + assertThat(dbKlass).isNotNull() + + // UPDATE + executeTransaction { + dbKlass?.accessFlags = AccessFlags.ACC_PUBLIC or AccessFlags.ACC_ABSTRACT + } + + // verify UPDATE + executeTransaction { + assertThat( + where(JavaClass::class.java) + .equalTo("fqn", "com/itsaky/androidide/indexing/TestClass") + .findFirst()?.accessFlags + ).isEqualTo(AccessFlags.ACC_PUBLIC or AccessFlags.ACC_ABSTRACT) + } + + // DELETE + executeTransaction { + RealmObject.deleteFromRealm(dbKlass!!) + } + + // verify DELTE + executeTransaction { + assertThat( + where(JavaClass::class.java) + .equalTo("fqn", "com/itsaky/androidide/indexing/TestClass") + .findFirst() + ).isNull() + } + } + } +} \ No newline at end of file diff --git a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt index 2e9c92528..b9b556611 100644 --- a/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt +++ b/lsp/java/src/androidTest/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexerTest.kt @@ -18,16 +18,10 @@ package com.itsaky.androidide.lsp.java.indexing import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.google.common.truth.Truth.assertThat -import io.realm.Realm -import io.realm.RealmConfiguration +import com.blankj.utilcode.util.ConvertUtils import io.realm.RealmModel -import io.realm.log.LogLevel -import io.realm.log.RealmLog import org.junit.Test import org.junit.runner.RunWith -import java.io.File /** * @author Akash Yadav @@ -36,50 +30,26 @@ import java.io.File class JavaIndexerTest { @Test - fun testSimpleRealmInit() { - val context = InstrumentationRegistry.getInstrumentation().context - Realm.init(context.applicationContext) + fun testSimpleAndroidJarIndexingDurationCheck() { + val helper = IndexingHelper("android-jar-classes.realm") + val worker = JavaIndexModelBuilder(helper.androidJar) + val batches = mutableMapOf, MutableList>() - val file = File.createTempFile("ajar", null, context.cacheDir) - context.assets.open("android.jar").use { asset -> - assertThat(asset).isNotNull() - file.outputStream().buffered().use { out -> - asset.copyTo(out) - out.flush() - } - } - - val dbDir = context.cacheDir.resolve("realm-db") - if (dbDir.exists()) { - if (dbDir.isDirectory) { - dbDir.deleteRecursively() - } else { - dbDir.delete() - } - } - - RealmLog.setLevel(LogLevel.ALL) - val realm = Realm.getInstance( - RealmConfiguration.Builder() - .directory(dbDir) - .name("android-jar-classes.realm") - .deleteRealmIfMigrationNeeded() - .build() - ) - - try { - val start = System.currentTimeMillis() - val worker = JavaIndexWorker(file) - val batches = mutableMapOf, MutableList>() + val totalDuration = 0L + var dbWriteDuration = 0L + helper.doWithRealm { + val totalStart = System.currentTimeMillis() worker.consumeTypes { type -> val batched = batches.computeIfAbsent(type.javaClass) { mutableListOf() } batched.add(type) if (batched.size >= 100) { - realm.executeTransaction { - realm.insertOrUpdate(batched) + val start = System.currentTimeMillis() + executeTransaction { + insertOrUpdate(batched) } + dbWriteDuration += System.currentTimeMillis() - start batched.clear() } @@ -87,16 +57,22 @@ class JavaIndexerTest { for ((_, batched) in batches) { if (batched.isNotEmpty()) { - realm.executeTransaction { - realm.insertOrUpdate(batched) + val start = System.currentTimeMillis() + executeTransaction { + insertOrUpdate(batched) } + dbWriteDuration += System.currentTimeMillis() - start } } - println("Took ${System.currentTimeMillis() - start}ms") - } finally { - realm.close() - file.delete() + println( + "Took ${dbWriteDuration}ms to write android.jar (${ + ConvertUtils.byte2FitMemorySize( + helper.androidJar.length() + ) + }) classes to Realm" + ) + println("Total time: ${System.currentTimeMillis() - totalStart}ms") } } } \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorker.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexModelBuilder.kt similarity index 98% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorker.kt rename to lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexModelBuilder.kt index da39b7833..9501a88b6 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorker.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexModelBuilder.kt @@ -60,12 +60,16 @@ import java.io.File import java.util.zip.ZipFile /** + * [JavaIndexModelBuilder] builds models for Java classes in a JAR file. + * * @author Akash Yadav */ -class JavaIndexWorker( +class JavaIndexModelBuilder( private val jar: File ) { + // for testing purposes + // prefer using consumeTypes(Function) fun buildTypes(): List> { val types = mutableListOf>() consumeTypes { types.add(it) } @@ -278,7 +282,8 @@ class JavaIndexWorker( if (value == null) { value = when (char) { 's' -> PrimitiveAnnotationElementValue.newInstance( - kind = IAnnotationElementValue.KIND_STRING, value = toPrimitiveConstant( + kind = IAnnotationElementValue.KIND_STRING, + value = toPrimitiveConstant( classFile.constant_pool, JavaType.KIND_REF, char, diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt index 36e0a174a..36879434e 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexer.kt @@ -45,7 +45,7 @@ class JavaIndexer( override fun index(index: IIndex, params: Params): Collection> { return jarPaths.map { jar -> coroutineScope.async { - val worker = JavaIndexWorker(jar) + val worker = JavaIndexModelBuilder(jar) // worker.index(index) } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/AnnotationElement.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/AnnotationElement.kt index 164490a46..109b6c1f9 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/AnnotationElement.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/AnnotationElement.kt @@ -36,11 +36,6 @@ import io.realm.annotations.Required @RealmClass(embedded = true) open class AnnotationElement : IJavaSymbol { - @Index - @Required - @RealmField("methodId") - var methodId: String? = null - @Index @Required @RealmField("name") diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaIndexingRealmModule.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaIndexingRealmModule.kt new file mode 100644 index 000000000..755affc93 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/indexing/models/JavaIndexingRealmModule.kt @@ -0,0 +1,46 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.java.indexing.models + +import io.realm.annotations.RealmModule + +/** + * [RealmModule] for the indexing databased for Java. + * + * @author Akash Yadav + */ +@RealmModule( + library = true, classes = [ + AnnotationElement::class, + AnnotationAnnotationElementValue::class, + ArrayAnnotationElementValue::class, + ClassAnnotationElementValue::class, + EnumAnnotationElementValue::class, + PrimitiveAnnotationElementValue::class, + ApiInfo::class, + JavaAnnotation::class, + JavaClass::class, + JavaConstant::class, + JavaEnum::class, + JavaField::class, + JavaInterface::class, + JavaMethod::class, + JavaType::class, + ] +) +internal class JavaIndexingRealmModule \ No newline at end of file diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarIndexingRule.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarModelingRule.kt similarity index 91% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarIndexingRule.kt rename to lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarModelingRule.kt index 1ce770605..b46ba07a0 100644 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarIndexingRule.kt +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/AndroidJarModelingRule.kt @@ -28,7 +28,7 @@ import java.io.File /** * @author Akash Yadav */ -class AndroidJarIndexingRule : TestRule { +class AndroidJarModelingRule : TestRule { private val androidJar: File by lazy { FileProvider.projectRoot() @@ -39,9 +39,9 @@ class AndroidJarIndexingRule : TestRule { .toFile() } - private val androidJarIndexer: JavaIndexWorker by lazy { + private val androidJarIndexer: JavaIndexModelBuilder by lazy { assertThat(androidJar.exists()).isTrue() - JavaIndexWorker(androidJar) + JavaIndexModelBuilder(androidJar) } val types: List> by lazy { diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt index 41b12f0c2..dad68efca 100644 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/indexing/JavaIndexWorkerText.kt @@ -22,9 +22,11 @@ import com.google.common.truth.Truth.assertThat import com.itsaky.androidide.lsp.java.indexing.models.AnnotationElement import com.itsaky.androidide.lsp.java.indexing.models.IAnnotationElementValue import com.itsaky.androidide.lsp.java.indexing.models.IJavaType +import com.itsaky.androidide.lsp.java.indexing.models.JavaAnnotation import com.itsaky.androidide.lsp.java.indexing.models.JavaClass import com.itsaky.androidide.lsp.java.indexing.models.JavaEnum import com.itsaky.androidide.lsp.java.indexing.models.JavaField +import com.itsaky.androidide.lsp.java.indexing.models.JavaInterface import com.itsaky.androidide.lsp.java.indexing.models.JavaMethod import com.itsaky.androidide.lsp.java.indexing.models.JavaType import com.itsaky.androidide.lsp.java.indexing.models.PrimitiveAnnotationElementValue @@ -40,11 +42,11 @@ import java.lang.reflect.Modifier * @author Akash Yadav */ @RunWith(RobolectricTestRunner::class) -class JavaIndexWorkerTest { +class JavaIndexModelBuilderTest { @Rule @JvmField - val indexRule = AndroidJarIndexingRule() + val indexRule = AndroidJarModelingRule() private val types: List> get() = indexRule.types @@ -405,7 +407,7 @@ class JavaIndexWorkerTest { } @Test - fun `test annotation methods with default integer value`() { + fun `test annotation methods with default values`() { types.firstOrNull { it.fqn == "java/lang/Deprecated" }.also { annotation -> assertThat(annotation?.methods).isNotEmpty() @@ -472,4 +474,52 @@ class JavaIndexWorkerTest { assertThat(defValue).isNull() } } + + @Test + fun `test class type classification`() { + types.firstOrNull { it.fqn == "android/view/View" }.also { + assertThat(it).isNotNull() + assertThat(it?.isClass).isTrue() + assertThat(it?.isInterface).isFalse() + assertThat(it?.isAnnotation).isFalse() + assertThat(it?.isEnum).isFalse() + assertThat(it).isInstanceOf(JavaClass::class.java) + } + } + + @Test + fun `test interface type classification`() { + types.firstOrNull { it.fqn == "android/view/View\$OnClickListener" }.also { + assertThat(it).isNotNull() + assertThat(it?.isClass).isFalse() + assertThat(it?.isInterface).isTrue() + assertThat(it?.isAnnotation).isFalse() + assertThat(it?.isEnum).isFalse() + assertThat(it).isInstanceOf(JavaInterface::class.java) + } + } + + @Test + fun `test annotation type classification`() { + types.firstOrNull { it.fqn == "java/lang/Deprecated" }.also { + assertThat(it).isNotNull() + assertThat(it?.isClass).isFalse() + assertThat(it?.isInterface).isTrue() + assertThat(it?.isAnnotation).isTrue() + assertThat(it?.isEnum).isFalse() + assertThat(it).isInstanceOf(JavaAnnotation::class.java) + } + } + + @Test + fun `test enum type classification`() { + types.firstOrNull { it.fqn == "android/icu/text/DisplayContext" }.also { + assertThat(it).isNotNull() + assertThat(it?.isClass).isFalse() + assertThat(it?.isInterface).isFalse() + assertThat(it?.isAnnotation).isFalse() + assertThat(it?.isEnum).isTrue() + assertThat(it).isInstanceOf(JavaEnum::class.java) + } + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2260e4d3d..aa594273b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -141,6 +141,7 @@ include( ":subprojects:builder-model-impl", ":subprojects:flashbar", ":subprojects:framework-stubs", + ":subprojects:database", ":subprojects:indexing", ":subprojects:javac-services", ":subprojects:projects", diff --git a/subprojects/database/.gitignore b/subprojects/database/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/subprojects/database/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/subprojects/database/build.gradle.kts b/subprojects/database/build.gradle.kts new file mode 100644 index 000000000..4d89bdc3e --- /dev/null +++ b/subprojects/database/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-kapt") + id("realm-android") +} + +android { + namespace = "${BuildConfig.packageName}.database" +} + +dependencies { + kapt(libs.google.auto.service) + implementation(libs.google.auto.service.annotations) + + implementation(projects.logger) + implementation(projects.common) + implementation(projects.shared) +} \ No newline at end of file diff --git a/subprojects/database/consumer-rules.pro b/subprojects/database/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/subprojects/database/proguard-rules.pro b/subprojects/database/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/subprojects/database/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# 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 *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IRealmProvider.kt b/subprojects/database/src/main/java/com/itsaky/androidide/db/IRealmProvider.kt similarity index 97% rename from subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IRealmProvider.kt rename to subprojects/database/src/main/java/com/itsaky/androidide/db/IRealmProvider.kt index f347b3554..539d91208 100644 --- a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IRealmProvider.kt +++ b/subprojects/database/src/main/java/com/itsaky/androidide/db/IRealmProvider.kt @@ -15,7 +15,7 @@ * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.indexing +package com.itsaky.androidide.db import com.itsaky.androidide.utils.ServiceLoader import io.realm.Realm diff --git a/app/src/main/java/com/itsaky/androidide/indexing/DatabaseEntity.kt b/subprojects/database/src/main/java/com/itsaky/androidide/db/internal/DatabaseEntity.kt similarity index 62% rename from app/src/main/java/com/itsaky/androidide/indexing/DatabaseEntity.kt rename to subprojects/database/src/main/java/com/itsaky/androidide/db/internal/DatabaseEntity.kt index 41a264e24..0974859f5 100644 --- a/app/src/main/java/com/itsaky/androidide/indexing/DatabaseEntity.kt +++ b/subprojects/database/src/main/java/com/itsaky/androidide/db/internal/DatabaseEntity.kt @@ -15,7 +15,7 @@ * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.indexing +package com.itsaky.androidide.db.internal import io.realm.RealmObject import io.realm.annotations.PrimaryKey @@ -27,7 +27,7 @@ import io.realm.annotations.Required * * @author Akash Yadav */ -data class DatabaseEntity( +open class DatabaseEntity( @Required @PrimaryKey @RealmField("path") @@ -40,4 +40,27 @@ data class DatabaseEntity( @Required @RealmField("directory") var directory: String = "", -) : RealmObject() \ No newline at end of file +) : RealmObject() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DatabaseEntity) return false + + if (path != other.path) return false + if (name != other.name) return false + if (directory != other.directory) return false + + return true + } + + override fun hashCode(): Int { + var result = path.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + directory.hashCode() + return result + } + + override fun toString(): String { + return "DatabaseEntity(path='$path', name='$name', directory='$directory')" + } +} \ No newline at end of file diff --git a/subprojects/database/src/main/java/com/itsaky/androidide/db/internal/InternalDBRealmModule.kt b/subprojects/database/src/main/java/com/itsaky/androidide/db/internal/InternalDBRealmModule.kt new file mode 100644 index 000000000..4c5a9a7dd --- /dev/null +++ b/subprojects/database/src/main/java/com/itsaky/androidide/db/internal/InternalDBRealmModule.kt @@ -0,0 +1,28 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.db.internal + +import io.realm.annotations.RealmModule + +/** + * [RealmModule] for the internal database entitites. + * + * @author Akash Yadav + */ +@RealmModule(library = true, classes = [DatabaseEntity::class]) +internal class InternalDBRealmModule \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/indexing/RealmProviderImpl.kt b/subprojects/database/src/main/java/com/itsaky/androidide/db/internal/RealmProviderImpl.kt similarity index 78% rename from app/src/main/java/com/itsaky/androidide/indexing/RealmProviderImpl.kt rename to subprojects/database/src/main/java/com/itsaky/androidide/db/internal/RealmProviderImpl.kt index 0ab5053a2..4741b8f70 100644 --- a/app/src/main/java/com/itsaky/androidide/indexing/RealmProviderImpl.kt +++ b/subprojects/database/src/main/java/com/itsaky/androidide/db/internal/RealmProviderImpl.kt @@ -15,9 +15,10 @@ * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.indexing +package com.itsaky.androidide.db.internal import com.google.auto.service.AutoService +import com.itsaky.androidide.db.IRealmProvider import com.itsaky.androidide.utils.Environment import io.realm.Realm import io.realm.RealmConfiguration @@ -30,7 +31,7 @@ import org.slf4j.LoggerFactory */ @AutoService(IRealmProvider::class) @Suppress("unused") -class RealmProviderImpl : IRealmProvider { +internal class RealmProviderImpl : IRealmProvider { companion object { private val log = LoggerFactory.getLogger(RealmProviderImpl::class.java) @@ -38,10 +39,12 @@ class RealmProviderImpl : IRealmProvider { // must be a relative path, with no special characters except '_' '-' and '/' (path sep) private val dbNameRegex = Regex("^[a-zA-Z0-9_\\-/]+$") private const val MASTER_DB_NAME = "master" - private const val MASTER_DB_PATH = "/${MASTER_DB_NAME}" + private const val MASTER_DB_PATH = "/$MASTER_DB_NAME" } - private val masterDb = createDb(MASTER_DB_PATH, null) + private val masterDb: Realm by lazy { + createDb(MASTER_DB_PATH, null) + } override fun get(path: String, config: (RealmConfiguration.Builder.() -> Unit)?): Realm { require(path != MASTER_DB_PATH) { @@ -73,24 +76,30 @@ class RealmProviderImpl : IRealmProvider { log.info("Creating DB at $dbDir/$dbName") - masterDb.executeTransactionAsync { realm -> - realm.insertOrUpdate(DatabaseEntity(name = name, path = path, directory = dbDir.absolutePath)) + if (parentPath.isEmpty() && name != MASTER_DB_NAME) { + masterDb.executeTransactionAsync { realm -> + realm.insertOrUpdate(DatabaseEntity(name = name, path = path, directory = dbDir.absolutePath)) + } } return Realm.getInstance(config) } private fun validateDbPath(path: String): Pair { - require(path.matches(dbNameRegex)) { - "Invalid DB path: $path" - } - val idx = path.indexOfLast { it == IRealmProvider.PATH_SEPARATOR } - require(idx > 0) { + require( + path.matches(dbNameRegex) + && path[0] == IRealmProvider.PATH_SEPARATOR + && idx >= 0 + && path.indexOf("//") == -1 + ) { "Invalid DB path: $path" } - val parentPath = path.substring(0, idx) + val parentPath = if (idx > 1) { + path.substring(1, idx) + } else "" + val name = path.substring(idx + 1) if (name == MASTER_DB_NAME) { diff --git a/subprojects/indexing/build.gradle.kts b/subprojects/indexing/build.gradle.kts index f02a6b14a..519172d8f 100644 --- a/subprojects/indexing/build.gradle.kts +++ b/subprojects/indexing/build.gradle.kts @@ -35,4 +35,5 @@ dependencies { api(projects.logger) api(projects.common) api(projects.shared) + api(projects.subprojects.database) } \ No newline at end of file diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndex.kt b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndex.kt index 4b983653e..a31568765 100644 --- a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndex.kt +++ b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IIndex.kt @@ -17,6 +17,7 @@ package com.itsaky.androidide.indexing +import com.itsaky.androidide.db.IRealmProvider import com.itsaky.androidide.indexing.internal.DefaultIndex /** diff --git a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IndexProvider.kt b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IndexProvider.kt index 5590c9fda..5d8fcfc3c 100644 --- a/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IndexProvider.kt +++ b/subprojects/indexing/src/main/java/com/itsaky/androidide/indexing/IndexProvider.kt @@ -17,7 +17,8 @@ package com.itsaky.androidide.indexing -import com.itsaky.androidide.indexing.IRealmProvider.Companion.PATH_SEPARATOR +import com.itsaky.androidide.db.IRealmProvider +import com.itsaky.androidide.db.IRealmProvider.Companion.PATH_SEPARATOR import com.itsaky.androidide.indexing.internal.DefaultIndex import io.realm.RealmConfiguration