From 1fd853cd172d9929ad87ef4035bb413f3e60f59f Mon Sep 17 00:00:00 2001 From: Stevek Date: Wed, 8 Apr 2026 18:59:01 +0200 Subject: [PATCH 1/7] Add documents. --- .../jecnaapi/java/JecnaClientJavaWrapper.kt | 4 +- .../io/github/tomhula/jecnaapi/JecnaClient.kt | 3 ++ .../github/tomhula/jecnaapi/WebJecnaClient.kt | 13 +++-- .../jecnaapi/data/document/SchoolDocument.kt | 27 ++++++++++ .../parser/parsers/DocumentsPageParser.kt | 53 +++++++++++++++++++ 5 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt create mode 100644 src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt diff --git a/jecnaapi-java/src/jvmMain/kotlin/io/github/tomhula/jecnaapi/java/JecnaClientJavaWrapper.kt b/jecnaapi-java/src/jvmMain/kotlin/io/github/tomhula/jecnaapi/java/JecnaClientJavaWrapper.kt index 4f81047..a28598a 100644 --- a/jecnaapi-java/src/jvmMain/kotlin/io/github/tomhula/jecnaapi/java/JecnaClientJavaWrapper.kt +++ b/jecnaapi-java/src/jvmMain/kotlin/io/github/tomhula/jecnaapi/java/JecnaClientJavaWrapper.kt @@ -62,7 +62,9 @@ class JecnaClientJavaWrapper(autoLogin: Boolean = false) fun getNotifications() = GlobalScope.future { wrappedClient.getNotifications() } fun getRoomsPage() = GlobalScope.future { wrappedClient.getRoomsPage() } fun getRoom(roomReference: RoomReference) = GlobalScope.future { wrappedClient.getRoom(roomReference) } - fun getRoom(roomCode: String) = GlobalScope.future { wrappedClient.getRoom(roomCode) } + fun getRoom(roomCode: String) = GlobalScope.future { wrappedClient.getRoom(roomCode) } + fun getDocumentsPage() = GlobalScope.future { wrappedClient.getDocumentsPage() } + fun getDocumentsPage(path: String) = GlobalScope.future { wrappedClient.getDocumentsPage(path) } /** A query without any authentication (autologin) handling. */ fun plainQuery(path: String, parameters: Parameters? = null) = diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt index 33a524c..9d40c05 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/JecnaClient.kt @@ -1,4 +1,6 @@ package io.github.tomhula.jecnaapi + +import io.github.tomhula.jecnaapi.data.document.DocumentsPage import io.github.tomhula.jecnaapi.data.absence.AbsencesPage import io.github.tomhula.jecnaapi.data.article.NewsPage import io.github.tomhula.jecnaapi.data.attendance.AttendancesPage @@ -52,6 +54,7 @@ interface JecnaClient /** @return the list of student's certificates or `null` if the student cannot have any certificates (i.e. he is not in the 4th grade) */ suspend fun getStudentCertificates(): List? + suspend fun getDocumentsPage(path: String = "/dokumenty"): DocumentsPage companion object { diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index 530a949..0e7eca3 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -3,6 +3,7 @@ package io.github.tomhula.jecnaapi import com.fleeksoft.ksoup.Ksoup import com.fleeksoft.ksoup.nodes.Document import io.github.tomhula.jecnaapi.data.cert.Certificate +import io.github.tomhula.jecnaapi.data.document.DocumentsPage import io.github.tomhula.jecnaapi.data.notification.NotificationReference import io.github.tomhula.jecnaapi.parser.parsers.* import io.github.tomhula.jecnaapi.util.JecnaPeriodEncoder @@ -92,6 +93,7 @@ class WebJecnaClient( private val roomsPageParser = RoomsPageParser private val roomParser = RoomParser(TimetableParser) private val certificatePageParser = CertificatePageParser + private val documentsPageParser = DocumentsPageParser @OptIn(ExperimentalTime::class) override suspend fun login(auth: Auth): Boolean @@ -172,15 +174,12 @@ class WebJecnaClient( override suspend fun getNotifications() = notificationParser.parse(queryStringBody(PageWebPath.NOTIFICATIONS)) override suspend fun getStudentCertificates(): List? { - val response = query(PageWebPath.CERTIFICATES) - val locationHeader = response.headers[HttpHeaders.Location] - - if (locationHeader == "$endpoint/neopravneny-pristup") - return null - - return certificatePageParser.parse(response.bodyAsText()) + return certificatePageParser.parse(queryStringBody(PageWebPath.CERTIFICATES)) } + override suspend fun getDocumentsPage(path: String): DocumentsPage = + documentsPageParser.parse(queryStringBody(path)) + suspend fun setRole(role: Role) { /* Refer to Role section in /internal_docs/Jecna_server.md */ diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt new file mode 100644 index 0000000..f01005a --- /dev/null +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt @@ -0,0 +1,27 @@ +package io.github.tomhula.jecnaapi.data.document + +import kotlinx.serialization.Serializable + +@Serializable +sealed class SchoolDocument +{ + abstract val label: String +} + +@Serializable +data class DocumentFolder( + override val label: String, + val path: String +) : SchoolDocument() + +@Serializable +data class DocumentFile( + override val label: String, + val downloadPath: String +) : SchoolDocument() + +@Serializable +data class DocumentsPage( + val path: String, + val documents: List +) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt new file mode 100644 index 0000000..063a930 --- /dev/null +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt @@ -0,0 +1,53 @@ +package io.github.tomhula.jecnaapi.parser.parsers + +import com.fleeksoft.ksoup.Ksoup +import com.fleeksoft.ksoup.nodes.Element +import io.github.tomhula.jecnaapi.data.document.DocumentFile +import io.github.tomhula.jecnaapi.data.document.DocumentFolder +import io.github.tomhula.jecnaapi.data.document.DocumentsPage +import io.github.tomhula.jecnaapi.data.document.SchoolDocument +import io.github.tomhula.jecnaapi.parser.ParseException + +internal object DocumentsPageParser +{ + fun parse(html: String): DocumentsPage + { + try + { + val document = Ksoup.parse(html) + val path = document.selectFirst(".documentPath") + ?.text() + ?.removePrefix("Adresa:") + ?.trim() + ?: "/dokumenty" + + val documents = document + .select("ul.documents a.dir, ul.documents a.file") + .mapNotNull { parseDocument(it) } + + return DocumentsPage(path, documents) + } catch (e: Exception) + { + throw ParseException("Failed to parse documents page.", e) + } + } + + private fun parseDocument(linkElement: Element): SchoolDocument? + { + val label = linkElement.selectFirst(".label") + ?.text() + ?.replace("\u00A0", " ") + ?: return null + + if (label == "..") return null + + val href = linkElement.attr("href") + + return when + { + linkElement.hasClass("dir") -> DocumentFolder(label, href) + linkElement.hasClass("file") -> DocumentFile(label, href) + else -> null + } + } +} From 713a34f5321b5fb4e68ed89f21133912f2b959cc Mon Sep 17 00:00:00 2001 From: Stevek Date: Thu, 9 Apr 2026 15:27:53 +0200 Subject: [PATCH 2/7] Fix revert --- .../kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index 0e7eca3..05b6152 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -174,7 +174,13 @@ class WebJecnaClient( override suspend fun getNotifications() = notificationParser.parse(queryStringBody(PageWebPath.NOTIFICATIONS)) override suspend fun getStudentCertificates(): List? { - return certificatePageParser.parse(queryStringBody(PageWebPath.CERTIFICATES)) + val response = query(PageWebPath.CERTIFICATES) + val locationHeader = response.headers[HttpHeaders.Location] + + if (locationHeader == "$endpoint/neopravneny-pristup") + return null + + return certificatePageParser.parse(response.bodyAsText()) } override suspend fun getDocumentsPage(path: String): DocumentsPage = From f37f7cf0d79886843c5690d1fa6547a9451001ed Mon Sep 17 00:00:00 2001 From: Stevek Date: Thu, 9 Apr 2026 15:30:04 +0200 Subject: [PATCH 3/7] rename --- .../github/tomhula/jecnaapi/data/document/SchoolDocument.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt index f01005a..ec95129 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt @@ -6,18 +6,19 @@ import kotlinx.serialization.Serializable sealed class SchoolDocument { abstract val label: String + abstract val path: String } @Serializable data class DocumentFolder( override val label: String, - val path: String + override val path: String ) : SchoolDocument() @Serializable data class DocumentFile( override val label: String, - val downloadPath: String + override val path: String ) : SchoolDocument() @Serializable From dd31dc68fca544a58bdebc828fd2033570dbc837 Mon Sep 17 00:00:00 2001 From: Stevek Date: Thu, 9 Apr 2026 15:43:06 +0200 Subject: [PATCH 4/7] Add parentPath --- .../jecnaapi/data/document/SchoolDocument.kt | 1 + .../parser/parsers/DocumentsPageParser.kt | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt index ec95129..d0b3deb 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/data/document/SchoolDocument.kt @@ -24,5 +24,6 @@ data class DocumentFile( @Serializable data class DocumentsPage( val path: String, + val parentPath: String? = null, val documents: List ) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt index 063a930..99f8a06 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt @@ -21,11 +21,24 @@ internal object DocumentsPageParser ?.trim() ?: "/dokumenty" + val parentPath = document + .select("ul.documents a.dir") + .firstOrNull { link -> + val label = link.selectFirst(".label") + ?.text() + ?.replace("\u00A0", " ") + ?: return@firstOrNull false + + label == ".." + } + ?.attr("href") + ?.takeIf { it.isNotBlank() } + val documents = document .select("ul.documents a.dir, ul.documents a.file") .mapNotNull { parseDocument(it) } - return DocumentsPage(path, documents) + return DocumentsPage(path = path, parentPath = parentPath, documents = documents) } catch (e: Exception) { throw ParseException("Failed to parse documents page.", e) From aae439c4079a6a66a120631fdfbca6c2aeab81e2 Mon Sep 17 00:00:00 2001 From: Stevek Date: Thu, 9 Apr 2026 18:37:34 +0200 Subject: [PATCH 5/7] Improve the efficiency of document parsing --- .../parser/parsers/DocumentsPageParser.kt | 55 +++++++------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt index 99f8a06..13d4224 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt @@ -1,11 +1,9 @@ package io.github.tomhula.jecnaapi.parser.parsers import com.fleeksoft.ksoup.Ksoup -import com.fleeksoft.ksoup.nodes.Element import io.github.tomhula.jecnaapi.data.document.DocumentFile import io.github.tomhula.jecnaapi.data.document.DocumentFolder import io.github.tomhula.jecnaapi.data.document.DocumentsPage -import io.github.tomhula.jecnaapi.data.document.SchoolDocument import io.github.tomhula.jecnaapi.parser.ParseException internal object DocumentsPageParser @@ -21,22 +19,30 @@ internal object DocumentsPageParser ?.trim() ?: "/dokumenty" - val parentPath = document - .select("ul.documents a.dir") - .firstOrNull { link -> - val label = link.selectFirst(".label") + var parentPath: String? = null + val documents = document + .select("ul.documents a.dir, ul.documents a.file") + .mapNotNull { linkElement -> + val label = linkElement.selectFirst(".label") ?.text() ?.replace("\u00A0", " ") - ?: return@firstOrNull false - - label == ".." + ?: return@mapNotNull null + + val href = linkElement.attr("href") + + if (label == "..") + { + parentPath = href.takeIf { it.isNotBlank() } + return@mapNotNull null + } + + when + { + linkElement.hasClass("dir") -> DocumentFolder(label, href) + linkElement.hasClass("file") -> DocumentFile(label, href) + else -> null + } } - ?.attr("href") - ?.takeIf { it.isNotBlank() } - - val documents = document - .select("ul.documents a.dir, ul.documents a.file") - .mapNotNull { parseDocument(it) } return DocumentsPage(path = path, parentPath = parentPath, documents = documents) } catch (e: Exception) @@ -44,23 +50,4 @@ internal object DocumentsPageParser throw ParseException("Failed to parse documents page.", e) } } - - private fun parseDocument(linkElement: Element): SchoolDocument? - { - val label = linkElement.selectFirst(".label") - ?.text() - ?.replace("\u00A0", " ") - ?: return null - - if (label == "..") return null - - val href = linkElement.attr("href") - - return when - { - linkElement.hasClass("dir") -> DocumentFolder(label, href) - linkElement.hasClass("file") -> DocumentFile(label, href) - else -> null - } - } } From 9e7aa7edd45e6717575295a522f7175c22f478c6 Mon Sep 17 00:00:00 2001 From: Tomas Hula Date: Tue, 21 Apr 2026 10:24:47 +0200 Subject: [PATCH 6/7] Add comment explaining non-breaking space replacement --- .../tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt index 13d4224..a922b27 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/parser/parsers/DocumentsPageParser.kt @@ -25,6 +25,7 @@ internal object DocumentsPageParser .mapNotNull { linkElement -> val label = linkElement.selectFirst(".label") ?.text() + // Replace non-breaking space with regular space ?.replace("\u00A0", " ") ?: return@mapNotNull null From e2ba7e2454c06f0a4af6399474efcec6a6837887 Mon Sep 17 00:00:00 2001 From: Tomas Hula Date: Tue, 21 Apr 2026 10:25:05 +0200 Subject: [PATCH 7/7] Remove new-line --- .../kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt index 05b6152..a54cdb7 100644 --- a/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt +++ b/src/commonMain/kotlin/io/github/tomhula/jecnaapi/WebJecnaClient.kt @@ -182,9 +182,7 @@ class WebJecnaClient( return certificatePageParser.parse(response.bodyAsText()) } - - override suspend fun getDocumentsPage(path: String): DocumentsPage = - documentsPageParser.parse(queryStringBody(path)) + override suspend fun getDocumentsPage(path: String): DocumentsPage = documentsPageParser.parse(queryStringBody(path)) suspend fun setRole(role: Role) {