Add shared module

This commit is contained in:
2020-11-12 10:25:02 +01:00
parent 151b4dbc76
commit 6ab63a7ddb
57 changed files with 929 additions and 617 deletions

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="net.underdesk.circolapp.shared.android" />

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared
import kotlinx.coroutines.Dispatchers
actual object PlatformDispatcher {
actual val IO = Dispatchers.IO
}

View File

@@ -0,0 +1,11 @@
package net.underdesk.circolapp.shared.data
import android.content.Context
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.db.SqlDriver
actual class DatabaseDriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(AppDatabase.Schema, context, "circolapp.db")
}
}

View File

@@ -0,0 +1,18 @@
package net.underdesk.circolapp.shared.server
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
actual class KtorFactory actual constructor() {
actual fun createClient() = HttpClient(OkHttp) {
install(JsonFeature) {
serializer = KotlinxSerializer(
kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
}
)
}
}
}

View File

@@ -0,0 +1,24 @@
package net.underdesk.circolapp.shared.server.curie
import net.underdesk.circolapp.shared.data.Circular
import org.jsoup.Jsoup
actual class SpecificCurieServer actual constructor(private val curieServer: CurieServer) {
actual fun parseHtml(string: String): List<Circular> {
val document = Jsoup.parseBodyFragment(string)
val htmlList = document.getElementsByTag("ul")[0].getElementsByTag("a")
val list = ArrayList<Circular>()
htmlList.forEach { element ->
if (element.parents().size == 6) {
list.last().attachmentsNames.add(element.text())
list.last().attachmentsUrls.add(element.attr("href"))
} else if (element.parents().size == 4) {
list.add(curieServer.generateFromString(element.text(), element.attr("href")))
}
}
return list
}
}

View File

@@ -0,0 +1,27 @@
package net.underdesk.circolapp.shared.server.porporato
import net.underdesk.circolapp.shared.data.Circular
import org.jsoup.Jsoup
actual class SpecificPorporatoServer actual constructor(private val porporatoServer: PorporatoServer) {
actual fun parseHtml(string: String): List<Circular> {
val document = Jsoup.parseBodyFragment(string)
val htmlList = document.getElementsByTag("table")[2]
.getElementsByTag("td")[2]
.getElementsByTag("a")
val list = ArrayList<Circular>()
for (i in 0 until htmlList.size) {
list.add(
porporatoServer.generateFromString(
htmlList[i].text(),
htmlList[i].attr("href"),
i.toLong()
)
)
}
return list
}
}

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared
import kotlinx.coroutines.CoroutineDispatcher
expect object PlatformDispatcher {
val IO: CoroutineDispatcher
}

View File

@@ -0,0 +1,31 @@
/*
* Circolapp
* Copyright (C) 2019-2020 Matteo Schiff
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.underdesk.circolapp.shared.data
data class Circular(
val id: Long,
val school: Int,
val name: String,
val url: String,
val date: String,
var favourite: Boolean = false,
var reminder: Boolean = false,
val attachmentsNames: MutableList<String> = mutableListOf(),
val attachmentsUrls: MutableList<String> = mutableListOf()
)

View File

@@ -0,0 +1,87 @@
package net.underdesk.circolapp.shared.data
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList
import kotlinx.coroutines.withContext
import net.underdesk.circolapp.shared.PlatformDispatcher
import net.underdesk.circolapp.shared.utils.SqlUtils.joinToString
import net.underdesk.circolapp.shared.utils.SqlUtils.toBoolean
import net.underdesk.circolapp.shared.utils.SqlUtils.toList
import net.underdesk.circolapp.shared.utils.SqlUtils.toLong
class CircularDao(
database: AppDatabase
) {
private val appDatabaseQueries = database.appDatabaseQueries
private val circularMapper =
{ id: Long, school: Long, name: String, url: String, date: String, favourite: Long, reminder: Long, attachmentsNames: String, attachmentsUrls: String ->
Circular(
id,
school.toInt(),
name,
url,
date,
favourite.toBoolean(),
reminder.toBoolean(),
attachmentsNames.toList(),
attachmentsUrls.toList()
)
}
suspend fun insertAll(circulars: List<Circular>) = withContext(PlatformDispatcher.IO) {
circulars.forEach {
appDatabaseQueries.insertCircular(
it.id,
it.school.toLong(),
it.name,
it.url,
it.date,
it.favourite.toLong(),
it.reminder.toLong(),
it.attachmentsNames.joinToString(),
it.attachmentsUrls.joinToString()
)
}
}
suspend fun update(id: Long, school: Int, favourite: Boolean, reminder: Boolean) =
withContext(PlatformDispatcher.IO) {
appDatabaseQueries.updateCircular(
favourite.toLong(),
reminder.toLong(),
id,
school.toLong()
)
}
suspend fun deleteAll() = withContext(PlatformDispatcher.IO) {
appDatabaseQueries.deleteAllCirculars()
}
fun getCircular(id: Long, school: Int) = appDatabaseQueries.getCircular(id, school.toLong(), circularMapper).executeAsOne()
fun getCirculars(school: Int) =
appDatabaseQueries.getCirculars(school.toLong(), circularMapper).executeAsList()
fun getFlowCirculars(school: Int) =
appDatabaseQueries.getCirculars(school.toLong(), circularMapper).asFlow().mapToList()
fun searchCirculars(query: String, school: Int) =
appDatabaseQueries.searchCirculars(school.toLong(), query, circularMapper).asFlow()
.mapToList()
fun getFavourites(school: Int) =
appDatabaseQueries.getFavourites(school.toLong(), circularMapper).asFlow().mapToList()
fun searchFavourites(query: String, school: Int) =
appDatabaseQueries.searchFavourites(school.toLong(), query, circularMapper).asFlow()
.mapToList()
fun getReminders(school: Int) =
appDatabaseQueries.getReminders(school.toLong(), circularMapper).asFlow().mapToList()
fun searchReminders(query: String, school: Int) =
appDatabaseQueries.searchReminders(school.toLong(), query, circularMapper).asFlow()
.mapToList()
}

View File

@@ -0,0 +1,36 @@
package net.underdesk.circolapp.shared.data
import net.underdesk.circolapp.shared.server.ServerAPI
class CircularRepository(
val circularDao: CircularDao,
private val serverAPI: ServerAPI
) {
suspend fun updateCirculars(returnNewCirculars: Boolean = true): Pair<List<Circular>, Boolean> {
var onlyNewCirculars = listOf<Circular>()
val result = serverAPI.getCircularsFromServer()
if (result.second == ServerAPI.Companion.Result.ERROR)
return Pair(emptyList(), false)
val oldCirculars = circularDao.getCirculars(serverAPI.serverID())
val newCirculars = result.first
if (newCirculars.size != oldCirculars.size) {
if (newCirculars.size < oldCirculars.size) {
circularDao.deleteAll()
}
if (returnNewCirculars) {
val oldCircularsSize =
if (newCirculars.size < oldCirculars.size) 0 else oldCirculars.size
val circularCount = newCirculars.size - oldCircularsSize
onlyNewCirculars = newCirculars.subList(0, circularCount)
}
circularDao.insertAll(newCirculars)
}
return Pair(onlyNewCirculars, true)
}
}

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared.data
import com.squareup.sqldelight.db.SqlDriver
expect class DatabaseDriverFactory {
fun createDriver(): SqlDriver
}

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared.server
import io.ktor.client.*
expect class KtorFactory() {
fun createClient(): HttpClient
}

View File

@@ -0,0 +1,27 @@
/*
* Circolapp
* Copyright (C) 2019-2020 Matteo Schiff
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.underdesk.circolapp.shared.server
import net.underdesk.circolapp.shared.data.Circular
abstract class Server {
abstract val serverID: Int
abstract suspend fun getCircularsFromServer(): Pair<List<Circular>, ServerAPI.Companion.Result>
abstract suspend fun newCircularsAvailable(): Pair<Boolean, ServerAPI.Companion.Result>
}

View File

@@ -0,0 +1,71 @@
/*
* Circolapp
* Copyright (C) 2019-2020 Matteo Schiff
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.underdesk.circolapp.shared.server
import kotlinx.coroutines.withContext
import net.underdesk.circolapp.shared.PlatformDispatcher
import net.underdesk.circolapp.shared.data.Circular
import net.underdesk.circolapp.shared.server.curie.CurieServer
import net.underdesk.circolapp.shared.server.porporato.PorporatoServer
class ServerAPI(
private var server: Server
) {
fun serverID(): Int = server.serverID
suspend fun getCircularsFromServer(): Pair<List<Circular>, Result> = withContext(PlatformDispatcher.IO) {
val newCircularsAvailable = server.newCircularsAvailable()
if (newCircularsAvailable.second == Result.ERROR)
return@withContext Pair(emptyList(), Result.ERROR)
if (!newCircularsAvailable.first)
return@withContext Pair(emptyList(), Result.SUCCESS)
server.getCircularsFromServer()
}
fun changeServer(server: Server) {
this.server = server
}
companion object {
enum class Servers {
CURIE, PORPORATO
}
enum class Result {
SUCCESS, ERROR
}
fun getServerId(server: Servers): Int {
return Servers.values().indexOf(server)
}
fun getServerName(server: Servers) = when (server) {
Servers.CURIE -> "Liceo scientifico Maria Curie"
Servers.PORPORATO -> "Liceo G.F. Porporato"
}
fun createServer(server: Servers) = when (server) {
Servers.CURIE -> CurieServer()
Servers.PORPORATO -> PorporatoServer()
}
}
}

View File

@@ -0,0 +1,73 @@
package net.underdesk.circolapp.shared.server.curie
import io.ktor.client.request.*
import io.ktor.utils.io.errors.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.underdesk.circolapp.shared.data.Circular
import net.underdesk.circolapp.shared.server.KtorFactory
import net.underdesk.circolapp.shared.server.Server
import net.underdesk.circolapp.shared.server.ServerAPI
import net.underdesk.circolapp.shared.server.pojo.Response
import kotlin.coroutines.cancellation.CancellationException
class CurieServer : Server() {
private val client = KtorFactory().createClient()
override val serverID = ServerAPI.getServerId(ServerAPI.Companion.Servers.CURIE)
override suspend fun getCircularsFromServer(): Pair<List<Circular>, ServerAPI.Companion.Result> {
return try {
withContext(Dispatchers.Default) {
val json = retrieveDataFromServer()
val list = SpecificCurieServer(this@CurieServer).parseHtml(json.content.rendered)
Pair(list, ServerAPI.Companion.Result.SUCCESS)
}
} catch (exception: IOException) {
Pair(emptyList(), ServerAPI.Companion.Result.ERROR)
}
}
override suspend fun newCircularsAvailable(): Pair<Boolean, ServerAPI.Companion.Result> {
return Pair(true, ServerAPI.Companion.Result.SUCCESS)
}
@OptIn(ExperimentalStdlibApi::class)
@Throws(IOException::class, CancellationException::class)
private suspend fun retrieveDataFromServer(): Response {
return client.get(ENDPOINT_URL)
}
fun generateFromString(string: String, url: String): Circular {
val idRegex =
"""(\d+)""".toRegex()
val idMatcher = idRegex.find(string)
val id = idMatcher?.value?.toLong() ?: -1L
val dateRegex =
"""(\d{2}/\d{2}/\d{4})""".toRegex()
val dateMatcher = dateRegex.find(string)
var title = string.removeSuffix("-signed")
return if (dateMatcher != null) {
title = title.removeRange(0, dateMatcher.range.last + 1)
.removePrefix(" ")
.removePrefix("_")
.removePrefix(" ")
Circular(id, serverID, title, url, dateMatcher.value)
} else {
Circular(id, serverID, title, url, "")
}
}
companion object {
const val ENDPOINT_URL = "https://www.curiepinerolo.edu.it/wp-json/wp/v2/pages/5958"
}
}
expect class SpecificCurieServer(curieServer: CurieServer) {
fun parseHtml(string: String): List<Circular>
}

View File

@@ -0,0 +1,13 @@
package net.underdesk.circolapp.shared.server.pojo
import kotlinx.serialization.Serializable
@Serializable
data class Response(
val content: Content
)
@Serializable
data class Content(
val rendered: String
)

View File

@@ -0,0 +1,140 @@
package net.underdesk.circolapp.shared.server.porporato
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.utils.io.charsets.*
import io.ktor.utils.io.errors.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.underdesk.circolapp.shared.data.Circular
import net.underdesk.circolapp.shared.server.KtorFactory
import net.underdesk.circolapp.shared.server.Server
import net.underdesk.circolapp.shared.server.ServerAPI
import kotlin.coroutines.cancellation.CancellationException
class PorporatoServer : Server() {
private val client = KtorFactory().createClient()
private val baseUrl = "https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/"
private val endpointUrls = listOf(
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-01-Settembre/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-02-Ottobre/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-03-Novembre/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-04-Dicembre/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-05-Gennaio/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-06-Febbraio/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-07-Marzo/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-08-Aprile/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-09-Maggio/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-10-Giugno/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-11-Luglio/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-12-Agosto/"
)
override val serverID = ServerAPI.getServerId(ServerAPI.Companion.Servers.PORPORATO)
override suspend fun getCircularsFromServer(): Pair<List<Circular>, ServerAPI.Companion.Result> {
return try {
val list = arrayListOf<Circular>()
for (url in endpointUrls) {
list.addAll(parsePage(url))
}
list.sortByDescending { it.id }
Pair(list, ServerAPI.Companion.Result.SUCCESS)
} catch (exception: IOException) {
Pair(emptyList(), ServerAPI.Companion.Result.ERROR)
}
}
override suspend fun newCircularsAvailable(): Pair<Boolean, ServerAPI.Companion.Result> {
return Pair(true, ServerAPI.Companion.Result.SUCCESS)
}
@OptIn(ExperimentalStdlibApi::class)
@Throws(IOException::class, CancellationException::class)
private suspend fun parsePage(url: String): List<Circular> {
val response = retrieveDataFromServer(url)
return withContext(Dispatchers.Default) {
val list = SpecificPorporatoServer(this@PorporatoServer).parseHtml(response).toMutableList()
// Identify and group all attachments
list.removeAll { attachment ->
if (attachment.name.startsWith("All", true)) {
val parent = list.find { it.id == attachment.id && !it.name.startsWith("All") }
parent?.attachmentsNames?.add(attachment.name)
parent?.attachmentsUrls?.add(attachment.url)
return@removeAll true
}
false
}
// Identify and group attachments not marked with "All"
var lastIndex = -1
var lastId = -1L
list.removeAll { attachment ->
if (lastId == attachment.id) {
val parent = list[lastIndex]
parent.attachmentsNames.add(attachment.name)
parent.attachmentsUrls.add(attachment.url)
return@removeAll true
}
lastId = attachment.id
lastIndex = list.indexOf(attachment)
false
}
list
}
}
@OptIn(ExperimentalStdlibApi::class)
@Throws(IOException::class, CancellationException::class)
private suspend fun retrieveDataFromServer(url: String): String {
return client.request<HttpResponse>(url).readText(Charsets.ISO_8859_1)
}
fun generateFromString(string: String, path: String, index: Long): Circular {
val fullUrl = baseUrl + path
var title = string
val idRegex =
"""(\d+)""".toRegex()
val idMatcher = idRegex.find(string)
val id = if (!string.startsWith("Avviso") && idMatcher != null) {
title = title.removeRange(idMatcher.range)
.removePrefix(" ")
.removePrefix("-")
.removePrefix(" ")
idMatcher.value.toLong()
} else {
-index
}
val dateRegex =
"""(\d{2}-\d{2}-\d{4})""".toRegex()
val dateMatcher = dateRegex.find(title)
return if (dateMatcher != null) {
title = title.removeRange(dateMatcher.range)
.removeSuffix(" (pubb.: )")
Circular(id, serverID, title, fullUrl, dateMatcher.value.replace("-", "/"))
} else {
Circular(id, serverID, title, fullUrl, "")
}
}
}
expect class SpecificPorporatoServer(porporatoServer: PorporatoServer) {
fun parseHtml(string: String): List<Circular>
}

View File

@@ -0,0 +1,29 @@
package net.underdesk.circolapp.shared.utils
object SqlUtils {
fun Boolean.toLong() = if (this) 1L else 0L
fun Long.toBoolean() = this == 1L
fun String?.toList(): MutableList<String> {
val list: MutableList<String> = mutableListOf()
if (this != null) {
for (attachment in this.split("˜")) {
list.add(attachment)
}
}
return list.dropLast(1).toMutableList()
}
fun List<String>.joinToString(): String {
var string = ""
for (attachment in this) {
string += "$attachment˜"
}
return string
}
}

View File

@@ -0,0 +1,59 @@
CREATE TABLE Circulars (
id INTEGER NOT NULL,
school INTEGER NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL,
date TEXT NOT NULL,
favourite INTEGER NOT NULL DEFAULT 0,
reminder INTEGER NOT NULL DEFAULT 0,
attachmentsNames TEXT NOT NULL,
attachmentsUrls TEXT NOT NULL,
PRIMARY KEY (id, school)
);
insertCircular:
INSERT OR IGNORE INTO Circulars(id, school, name, url, date, favourite, reminder, attachmentsNames, attachmentsUrls)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?);
updateCircular:
UPDATE Circulars
SET favourite = ?, reminder = ?
WHERE id = ? AND school = ?;
deleteAllCirculars:
DELETE FROM Circulars;
getCirculars:
SELECT * FROM Circulars
WHERE school IS ?
ORDER BY id DESC;
getCircular:
SELECT * FROM Circulars
WHERE id IS ? AND school = ?
ORDER BY id DESC;
searchCirculars:
SELECT * FROM Circulars
WHERE school IS ? AND name LIKE ?
ORDER BY id DESC;
getFavourites:
SELECT * FROM Circulars
WHERE school IS ? AND favourite
ORDER BY id DESC;
searchFavourites:
SELECT * FROM Circulars
WHERE school IS ? AND favourite AND name LIKE ?
ORDER BY id DESC;
getReminders:
SELECT * FROM Circulars
WHERE school IS ? AND reminder
ORDER BY id DESC;
searchReminders:
SELECT * FROM Circulars
WHERE school IS ? AND reminder AND name LIKE ?
ORDER BY id DESC;

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared
import kotlinx.coroutines.Dispatchers
actual object PlatformDispatcher {
actual val IO = Dispatchers.Default
}

View File

@@ -0,0 +1,10 @@
package net.underdesk.circolapp.shared.data
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
actual class DatabaseDriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(AppDatabase.Schema, "circolapp.db")
}
}

View File

@@ -0,0 +1,18 @@
package net.underdesk.circolapp.shared.server
import io.ktor.client.*
import io.ktor.client.engine.ios.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
actual class KtorFactory actual constructor() {
actual fun createClient() = HttpClient(Ios) {
install(JsonFeature) {
serializer = KotlinxSerializer(
kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
}
)
}
}
}

View File

@@ -0,0 +1,9 @@
package net.underdesk.circolapp.shared.server.curie
import net.underdesk.circolapp.shared.data.Circular
actual class SpecificCurieServer actual constructor(val curieServer: CurieServer) {
actual fun parseHtml(string: String): List<Circular> {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,9 @@
package net.underdesk.circolapp.shared.server.porporato
import net.underdesk.circolapp.shared.data.Circular
actual class SpecificPorporatoServer actual constructor(porporatoServer: PorporatoServer) {
actual fun parseHtml(string: String): List<Circular> {
TODO("Not yet implemented")
}
}