From bdf201762efae3c5bb520035e53b031179cbd34c Mon Sep 17 00:00:00 2001 From: lihenggui Date: Mon, 12 Feb 2024 11:26:46 -0800 Subject: [PATCH] Add sqldelight support and move source to the commonMain folder --- build-logic/convention/build.gradle.kts | 4 ++ .../KmpAndroidLibraryConventionPlugin.kt | 27 ------------ .../main/kotlin/KmpLibraryConventionPlugin.kt | 15 +++++++ .../main/kotlin/SqlDelightConventionPlugin.kt | 39 +++++++++++++++++ .../apps/nowinandroid/KotlinMultiplatform.kt | 8 +--- build.gradle.kts | 1 + core/database/build.gradle.kts | 43 ++++++++++++++----- .../{main => commonMain}/AndroidManifest.xml | 0 .../nowinandroid/core/database/DaosModule.kt | 0 .../core/database/DatabaseMigrations.kt | 0 .../core/database/DatabaseModule.kt | 0 .../nowinandroid/core/database/NiaDatabase.kt | 0 .../core/database/dao/NewsResourceDao.kt | 0 .../core/database/dao/NewsResourceFtsDao.kt | 0 .../core/database/dao/RecentSearchQueryDao.kt | 0 .../core/database/dao/TopicDao.kt | 0 .../core/database/dao/TopicFtsDao.kt | 0 .../core/database/model/NewsResourceEntity.kt | 0 .../database/model/NewsResourceFtsEntity.kt | 0 .../model/NewsResourceTopicCrossRef.kt | 0 .../database/model/PopulatedNewsResource.kt | 0 .../database/model/RecentSearchQueryEntity.kt | 0 .../core/database/model/TopicEntity.kt | 0 .../core/database/model/TopicFtsEntity.kt | 0 .../core/database/util/InstantConverter.kt | 0 core/model/build.gradle.kts | 4 ++ gradle/libs.versions.toml | 5 +++ 27 files changed, 101 insertions(+), 45 deletions(-) delete mode 100644 build-logic/convention/src/main/kotlin/KmpAndroidLibraryConventionPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/SqlDelightConventionPlugin.kt rename core/database/src/{main => commonMain}/AndroidManifest.xml (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/DaosModule.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseMigrations.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceEntity.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceTopicCrossRef.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/model/PopulatedNewsResource.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/model/RecentSearchQueryEntity.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicEntity.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt (100%) rename core/database/src/{main => commonMain}/kotlin/com/google/samples/apps/nowinandroid/core/database/util/InstantConverter.kt (100%) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 2c84a8003..308846d13 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -114,5 +114,9 @@ gradlePlugin { id = "nowinandroid.kmp.library" implementationClass = "KmpLibraryConventionPlugin" } + register("kmpAndroidLibrary") { + id = "nowinandroid.kmp.android.library" + implementationClass = "KmpAndroidLibraryConventionPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/KmpAndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpAndroidLibraryConventionPlugin.kt deleted file mode 100644 index e35d844ef..000000000 --- a/build-logic/convention/src/main/kotlin/KmpAndroidLibraryConventionPlugin.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * 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 - * - * https://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. - */ - -import com.google.samples.apps.nowinandroid.configureAndroidKotlinMultiplatform -import org.gradle.api.Plugin -import org.gradle.api.Project -class KmpAndroidLibraryConventionPlugin: Plugin { - override fun apply(target: Project) { - with(target) { - plugins.apply("org.jetbrains.kotlin.multiplatform") - configureAndroidKotlinMultiplatform() - } - } -} diff --git a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt index d2a73858c..7710ff73a 100644 --- a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt @@ -14,15 +14,30 @@ * limitations under the License. */ +import com.android.build.gradle.LibraryExtension +import com.google.samples.apps.nowinandroid.configureFlavors +import com.google.samples.apps.nowinandroid.configureGradleManagedDevices +import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinMultiplatform import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure class KmpLibraryConventionPlugin: Plugin { override fun apply(target: Project) { with(target) { + plugins.apply("com.android.library") plugins.apply("org.jetbrains.kotlin.multiplatform") configureKotlinMultiplatform() + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = 34 + configureFlavors(this) + configureGradleManagedDevices(this) + // The resource prefix is derived from the module name, + // so resources inside ":core:module1" must be prefixed with "core_module1_" + resourcePrefix = path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_").lowercase() + "_" + } } } } diff --git a/build-logic/convention/src/main/kotlin/SqlDelightConventionPlugin.kt b/build-logic/convention/src/main/kotlin/SqlDelightConventionPlugin.kt new file mode 100644 index 000000000..5d2c1b888 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/SqlDelightConventionPlugin.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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. + */ + +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.gradle.LibraryExtension +import com.google.samples.apps.nowinandroid.configureFlavors +import com.google.samples.apps.nowinandroid.configureGradleManagedDevices +import com.google.samples.apps.nowinandroid.configureKotlinAndroid +import com.google.samples.apps.nowinandroid.configurePrintApksTask +import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.kotlin +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +class SqlDelightConventionPlugin: Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("app.cash.sqldelight") + extensions.configure { + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinMultiplatform.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinMultiplatform.kt index 355e705b5..2fc1957d9 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinMultiplatform.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinMultiplatform.kt @@ -28,6 +28,7 @@ import org.jetbrains.kotlin.konan.target.HostManager internal fun Project.configureKotlinMultiplatform() { extensions.configure { jvm() + androidTarget() js { browser { @@ -83,10 +84,3 @@ internal fun Project.configureKotlinMultiplatform() { } } } - -internal fun Project.configureAndroidKotlinMultiplatform() { - extensions.configure { - androidTarget() - } - configureKotlinMultiplatform() -} diff --git a/build.gradle.kts b/build.gradle.kts index 1f329a53e..094a9eb3d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,4 +48,5 @@ plugins { alias(libs.plugins.room) apply false alias(libs.plugins.jetbrainsCompose) apply false alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.sqldelight.gradle.plugin) apply false } diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 4a6bcb66a..c663cd270 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,8 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.library) - alias(libs.plugins.nowinandroid.android.library.jacoco) - alias(libs.plugins.nowinandroid.android.hilt) - alias(libs.plugins.nowinandroid.android.room) + alias(libs.plugins.nowinandroid.kmp.library) + alias(libs.plugins.sqldelight.gradle.plugin) } android { @@ -29,10 +27,33 @@ android { namespace = "com.google.samples.apps.nowinandroid.core.database" } -dependencies { - api(projects.core.model) - - implementation(libs.kotlinx.datetime) - - androidTestImplementation(projects.core.testing) +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + api(projects.core.model) + implementation(libs.kotlinx.datetime) + } + } + val androidMain by getting { + dependencies { + implementation(libs.sqldelight.android.driver) + } + } + val nativeMain by getting { + dependencies { + implementation(libs.sqldelight.native.driver) + } + } + val jvmMain by getting { + dependencies { + implementation(libs.sqldelight.sqlite.driver) + } + } + val commonTest by getting { + dependencies { + implementation(libs.kotlin.test) + } + } + } } diff --git a/core/database/src/main/AndroidManifest.xml b/core/database/src/commonMain/AndroidManifest.xml similarity index 100% rename from core/database/src/main/AndroidManifest.xml rename to core/database/src/commonMain/AndroidManifest.xml diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DaosModule.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DaosModule.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DaosModule.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DaosModule.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseMigrations.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseMigrations.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseMigrations.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseMigrations.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceEntity.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceEntity.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceEntity.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceEntity.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceTopicCrossRef.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceTopicCrossRef.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceTopicCrossRef.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceTopicCrossRef.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/PopulatedNewsResource.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/PopulatedNewsResource.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/PopulatedNewsResource.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/PopulatedNewsResource.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/RecentSearchQueryEntity.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/RecentSearchQueryEntity.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/RecentSearchQueryEntity.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/RecentSearchQueryEntity.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicEntity.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicEntity.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicEntity.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicEntity.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/util/InstantConverter.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/util/InstantConverter.kt similarity index 100% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/util/InstantConverter.kt rename to core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/util/InstantConverter.kt diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index d7844ecc1..0cba90bc7 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -18,6 +18,10 @@ plugins { alias(libs.plugins.nowinandroid.kmp.library) } +android { + namespace = "com.google.samples.apps.nowinandroid.core.model" +} + kotlin { sourceSets { commonMain.dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 117fefa4c..816da6a51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,6 +70,7 @@ androidx-test-junit = "1.1.5" compose = "1.6.0" compose-plugin = "1.6.0-alpha01" junit = "4.13.2" +sqldelight = "2.0.1" [libraries] accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } @@ -156,6 +157,9 @@ androidx-material = { group = "com.google.android.material", name = "material", androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } +sqldelight-android-driver = { group = "app.cash.sqldelight", name = "android-driver", version.ref = "sqldelight" } +sqldelight-native-driver = { group = "app.cash.sqldelight", name = "native-driver", version.ref = "sqldelight" } +sqldelight-sqlite-driver = { group = "app.cash.sqldelight", name = "sqlite-driver", version.ref = "sqldelight" } # Dependencies of the included build-logic android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } @@ -186,6 +190,7 @@ room = { id = "androidx.room", version.ref = "room" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +sqldelight-gradle-plugin = { id = "app.cash.sqldelight", version.ref = "sqldelight" } # Plugins defined by this project nowinandroid-android-application = { id = "nowinandroid.android.application", version = "unspecified" }