From ae9bf2a5628cce1edb1172478b003f88033d2065 Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Wed, 13 Oct 2021 15:46:17 +0200 Subject: [PATCH 01/11] Update Kotlin to 1.5.31 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d5feda5..1c0f7fe 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ plugins { - id 'org.jetbrains.kotlin.jvm' version '1.4.10' - id 'org.jetbrains.kotlin.plugin.spring' version '1.4.10' id 'org.springframework.boot' version '2.3.3.RELEASE' id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id 'org.jetbrains.kotlin.jvm' version '1.5.31' + id 'org.jetbrains.kotlin.plugin.spring' version '1.5.31' } group 'org.mosad' From 22f17d10e03cb348d5e491b337019acec29291dc Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Wed, 13 Oct 2021 15:57:26 +0200 Subject: [PATCH 02/11] Timetable fixes - Only one request is made to get the timetable HTML document for parsing the timetable and the weekNumberYear - On timeouts or other errors, the cached data won't be overwritten with emptiness anymore - The scheduled updates will now also update the weekNumberYear --- .../org/mosad/thecitadelofricks/DataTypes.kt | 2 +- .../controller/CacheController.kt | 47 +++++++++++++------ .../hsoparser/TimetableParser.kt | 47 ++++++++++--------- .../hsoparser/TimetableParserTest.kt | 12 ++--- 4 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/DataTypes.kt b/src/main/kotlin/org/mosad/thecitadelofricks/DataTypes.kt index 69e72b5..86160fe 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/DataTypes.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/DataTypes.kt @@ -58,7 +58,7 @@ data class TimetableDay(val timeslots: Array> = Array(6) { Arr data class TimetableWeek(val days: Array = Array(6) { TimetableDay() }) -data class TimetableCourseMeta(var updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, val weekNumberYear: Int = 0, val link: String = "") +data class TimetableCourseMeta(var updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, var weekNumberYear: Int = 0, val link: String = "") data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeek = TimetableWeek()) diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt index 9c2ff53..76788ba 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt @@ -59,7 +59,7 @@ class CacheController { * @param weekIndex request week number (current week = 0) * @return timetable of the course (Type: [TimetableCourseWeek]) */ - fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek = runBlocking { + fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek { // TODO just for testing if (courseName == "TEST_A" || courseName == "TEST_B") { @@ -68,27 +68,43 @@ class CacheController { val weekNumberYear = 0 val instr = javaClass.getResourceAsStream("/html/Timetable_normal-week.html") - val htmlDoc = Jsoup.parse(instr,"UTF-8", "https://www.hs-offenburg.de/") - val timetableTest = TimetableParser().parseTimeTable(htmlDoc) + val timetableParser = + TimetableParser(htmlDoc = Jsoup.parse(instr, "UTF-8", "https://www.hs-offenburg.de/")) + val timetableTest = timetableParser.parseTimeTable() - return@runBlocking TimetableCourseWeek(TimetableCourseMeta(currentTime, courseName, weekIndex, weekNumberYear, timetableLink), timetableTest) + return TimetableCourseWeek( + TimetableCourseMeta( + currentTime, + courseName, + weekIndex, + weekNumberYear, + timetableLink + ), timetableTest ?: TimetableWeek() + ) } - return@runBlocking timetableList.getOrPut("$courseName-$weekIndex") { + val key = "$courseName-$weekIndex" + return if (timetableList.contains(key)) { + timetableList[key]!! + } else { val timetableLink = courseList.courses[courseName] ?.courseLink ?.replace("week=0", "week=$weekIndex") ?: "" val currentTime = System.currentTimeMillis() / 1000 - var timetable = TimetableWeek() - var weekNumberYear = 0 - val timetableJobs = listOf( - async { timetable = TimetableParser().getTimeTable(timetableLink) }, - async { weekNumberYear = TimetableParser().getWeekNumberYear(timetableLink) } - ) + val timetableParser = TimetableParser(timetableLink) + val weekNumberYear = timetableParser.parseWeekNumberYear() + val timetable = timetableParser.parseTimeTable() - timetableJobs.awaitAll() - TimetableCourseWeek(TimetableCourseMeta(currentTime, courseName, weekIndex, weekNumberYear, timetableLink), timetable) + TimetableCourseWeek( + TimetableCourseMeta( + currentTime, + courseName, + weekIndex, + weekNumberYear ?: 0, + timetableLink + ), timetable ?: TimetableWeek() + ).also { timetableList[key] = it } } } @@ -177,7 +193,10 @@ class CacheController { try { timetableList.forEach { timetableCourse -> executor.execute { - timetableCourse.value.timetable = TimetableParser().getTimeTable(timetableCourse.value.meta.link) + val timetableParser = TimetableParser(timetableCourse.value.meta.link) + timetableCourse.value.timetable = timetableParser.parseTimeTable() ?: return@execute + timetableCourse.value.meta.weekNumberYear = + timetableParser.parseWeekNumberYear() ?: return@execute timetableCourse.value.meta.updateTime = System.currentTimeMillis() / 1000 saveTimetableToCache(timetableCourse.value) // save the updated timetable to the cache directory diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt index fd91e1f..c544530 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt @@ -28,26 +28,37 @@ import org.mosad.thecitadelofricks.Lesson import org.mosad.thecitadelofricks.TimetableWeek import org.slf4j.LoggerFactory -class TimetableParser { +/** + * @param timetableURL the URL of the timetable you want to get + * @param htmlDoc the html document to use (the timetableURL will be ignored if this value is present) + */ +class TimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) { private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableParser::class.java) private val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") + private val htmlDoc: Document? = + htmlDoc + ?: if (timetableURL == null) { + null + } else { + try { + Jsoup.connect(timetableURL).get() + } catch (gex: Exception) { + logger.error("general TimetableParser error", gex) + null + } + } + /** - * get the timetable from the given url + * parse the timetable from the previously given url * the timetable is organised per row not per column; * Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on - * @param timetableURL the URL of the timetable you want to get */ - fun getTimeTable(timetableURL: String): TimetableWeek { - return try { - parseTimeTable(Jsoup.connect(timetableURL).get()) - } catch (gex: Exception) { - logger.error("general TimetableParser error", gex) - TimetableWeek() + fun parseTimeTable(): TimetableWeek? { + if (htmlDoc == null) { + return null } - } - fun parseTimeTable(htmlDoc: Document): TimetableWeek { val timetableWeek = TimetableWeek() val rows = htmlDoc.select("table.timetable").select("tr[scope=\"row\"]") @@ -117,19 +128,13 @@ class TimetableParser { } /** - * get the week number of the year for the timetable - * @param timetableURL the URL of the timetable you want to get + * parse the week number of the year for the timetable */ - fun getWeekNumberYear(timetableURL: String): Int { - return try { - parseWeekNumberYear(Jsoup.connect(timetableURL).get()) - } catch (gex: Exception) { - logger.error("general TimetableParser error", gex) - 0 + fun parseWeekNumberYear(): Int? { + if (htmlDoc == null) { + return null } - } - fun parseWeekNumberYear(htmlDoc: Document): Int { return htmlDoc.select("h1.timetable-caption").text().substringAfter("- ") .substringBefore(".").replace(" ", "").toInt() } diff --git a/src/test/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParserTest.kt b/src/test/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParserTest.kt index 9bc7bde..8fb1ead 100644 --- a/src/test/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParserTest.kt +++ b/src/test/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParserTest.kt @@ -32,8 +32,8 @@ class TimetableParserTest { @Test fun parseTimetableNormalWeek() { val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) - val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") - val actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() + val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") + val actualTimetable = TimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_normal-week.txt").readText().trim() Assertions.assertEquals(expectedTimetable, actualTimetable) @@ -42,8 +42,8 @@ class TimetableParserTest { @Test fun parseTimetableEmptyWeek() { val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_empty-week.html").path) - val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") - val actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() + val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") + val actualTimetable = TimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_empty-week.txt").readText().trim() Assertions.assertEquals(expectedTimetable, actualTimetable) @@ -52,8 +52,8 @@ class TimetableParserTest { @Test fun parseWeekNumberYear() { val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) - val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") - val actualWeekNumberYear = TimetableParser().parseWeekNumberYear(htmlDoc) + val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") + val actualWeekNumberYear = TimetableParser(htmlDoc = htmlDoc).parseWeekNumberYear() Assertions.assertEquals(42, actualWeekNumberYear) } From a292b45fcbd65ce0a8d3ae02a625a557f7a165e7 Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Wed, 13 Oct 2021 16:02:35 +0200 Subject: [PATCH 03/11] Dependency updates This also replaces JCenter with Maven Central since JCenter is now read-only --- build.gradle | 14 +++++++------- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 1c0f7fe..88738b9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,25 +1,25 @@ plugins { - id 'org.springframework.boot' version '2.3.3.RELEASE' - id 'io.spring.dependency-management' version '1.0.9.RELEASE' id 'org.jetbrains.kotlin.jvm' version '1.5.31' id 'org.jetbrains.kotlin.plugin.spring' version '1.5.31' + id 'org.springframework.boot' version '2.5.5' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' } group 'org.mosad' version '1.2.8' repositories { - jcenter() + mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.jetbrains.kotlin:kotlin-stdlib' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8' - implementation 'org.jsoup:jsoup:1.13.1' - implementation 'com.google.code.gson:gson:2.8.6' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation 'org.jsoup:jsoup:1.14.3' + implementation 'com.google.code.gson:gson:2.8.8' - testImplementation 'org.junit.jupiter:junit-jupiter:5.6.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' } test { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 12d38de..ffed3a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 90847a2730d910e9ae1010d399fbab10a5cad5cf Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Wed, 13 Oct 2021 16:03:29 +0200 Subject: [PATCH 04/11] Also set JVM target to 11 for Java --- build.gradle | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 88738b9..1747233 100644 --- a/build.gradle +++ b/build.gradle @@ -30,9 +30,16 @@ test { } } +def jvmTargetVersion = "11" compileKotlin { - kotlinOptions.jvmTarget = "11" + kotlinOptions.jvmTarget = jvmTargetVersion +} +compileJava { + targetCompatibility = jvmTargetVersion } compileTestKotlin { - kotlinOptions.jvmTarget = "11" + kotlinOptions.jvmTarget = jvmTargetVersion +} +compileTestJava { + targetCompatibility = jvmTargetVersion } From 460d1ee131bd500bf56667c24521e78029783fec Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Wed, 13 Oct 2021 16:07:37 +0200 Subject: [PATCH 05/11] StatusController: use properties instead of getters --- .../controller/CachetAPIController.kt | 6 +-- .../controller/StatusController.kt | 39 ++++++------------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CachetAPIController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CachetAPIController.kt index a424572..534e3eb 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CachetAPIController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CachetAPIController.kt @@ -22,7 +22,7 @@ package org.mosad.thecitadelofricks.controller -import org.mosad.thecitadelofricks.controller.StatusController.Companion.getTotalRequests +import org.mosad.thecitadelofricks.controller.StatusController.Companion.totalRequests import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.BufferedReader @@ -42,8 +42,8 @@ class CachetAPIController { fun postTotalRequests() { try { val url = URL("${StartupController.cachetBaseURL}/api/v1/metrics/1/points") - val jsonInputString = "{\"value\": ${getTotalRequests() -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}" - oldTotalRequests = getTotalRequests() + val jsonInputString = "{\"value\": ${totalRequests -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}" + oldTotalRequests = totalRequests val con = url.openConnection() as HttpURLConnection con.requestMethod = "POST" diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt index b12420a..09f6da7 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt @@ -39,10 +39,14 @@ class StatusController { companion object { private val logger: Logger = LoggerFactory.getLogger(StatusController::class.java) - private var totalRequests = 0 - private var mensaMenuRequests = 0 - private var courseListRequests = 0 - private var timetableRequests = HashMap() + var totalRequests = 0 + private set + var mensaMenuRequests = 0 + private set + var courseListRequests = 0 + private set + var timetableRequests = HashMap() + private set /** * if a mensamenu/courseList/timetable is requested update the specific and total request count @@ -62,25 +66,6 @@ class StatusController { totalRequests++ } - /** - * getters and setters - */ - fun getTotalRequests(): Int { - return totalRequests - } - - fun getMensaMenuRequests(): Int { - return mensaMenuRequests - } - - fun getCourseListRequests(): Int { - return courseListRequests - } - - fun getTimetableRequests(): HashMap { - return timetableRequests - } - fun getStatus(): Status { val currentTime = System.currentTimeMillis() / 1000 val minutes = (currentTime - startTime) % 3600 / 60 @@ -112,10 +97,10 @@ class StatusController { "$days days, $hours:$minutes", apiVersion, softwareVersion, - getTotalRequests(), - getMensaMenuRequests(), - getCourseListRequests(), - getTimetableRequests(), + totalRequests, + mensaMenuRequests, + courseListRequests, + timetableRequests, CacheController.timetableList.size, Date(CacheController.courseList.meta.updateTime * 1000), Date(CacheController.mensaMenu.meta.updateTime * 1000), From f9cc9b5e14d376606f4dd0f82a75e3fb37d77e56 Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Wed, 13 Oct 2021 16:09:13 +0200 Subject: [PATCH 06/11] Make the update scheduling more readable (hopefully) --- build.gradle | 1 + .../controller/CacheController.kt | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 1747233..3cf7e2a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ test { def jvmTargetVersion = "11" compileKotlin { kotlinOptions.jvmTarget = jvmTargetVersion + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } compileJava { targetCompatibility = jvmTargetVersion diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt index 76788ba..24d4ddd 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt @@ -38,6 +38,8 @@ import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.collections.HashSet import kotlin.concurrent.scheduleAtFixedRate +import kotlin.time.Duration +import kotlin.time.ExperimentalTime class CacheController { @@ -247,30 +249,39 @@ class CacheController { * update the CourseList every 24h, the Timetables every 3h and the Mensa Menu every hour * doesn't account the change between winter and summer time! */ + @OptIn(ExperimentalTime::class) private fun scheduledUpdates() { val currentTime = System.currentTimeMillis() - val initDelay24h = (86400000 - ((currentTime + 3600000) % 86400000)) + 60000 - val initDelay3h = (10800000 - ((currentTime + 3600000) % 10800000)) + 60000 - val initDelay1h = (3600000 - ((currentTime + 3600000) % 3600000)) + 60000 + + val duration24h = Duration.hours(24).inWholeMilliseconds + val duration3h = Duration.hours(3).inWholeMilliseconds + val duration1h = Duration.hours(1).inWholeMilliseconds + val duration1m = Duration.minutes(1).inWholeMilliseconds + + // Calculate the initial delay to make the update time independent of the start time + fun calcInitDelay(period: Long) = (period - ((currentTime + duration1h) % period)) + duration1m + val initDelay24h = calcInitDelay(duration24h) + val initDelay3h = calcInitDelay(duration3h) + val initDelay1h = calcInitDelay(duration1h) // update courseList every 24 hours (time in ms) - Timer().scheduleAtFixedRate(initDelay24h, 86400000) { + Timer().scheduleAtFixedRate(initDelay24h, duration24h) { asyncUpdateCourseList() } // update all already existing timetables every 3 hours (time in ms) - Timer().scheduleAtFixedRate(initDelay3h, 10800000) { + Timer().scheduleAtFixedRate(initDelay3h, duration3h) { asyncUpdateTimetables() } // update courses every hour (time in ms) - Timer().scheduleAtFixedRate(initDelay1h, 3600000) { + Timer().scheduleAtFixedRate(initDelay1h, duration1h) { asyncUpdateMensa() } // post to status.mosad.xyz every hour, if an API key is present if (StartupController.cachetAPIKey != "0") { - Timer().scheduleAtFixedRate(initDelay1h, 3600000) { + Timer().scheduleAtFixedRate(initDelay1h, duration1h) { CachetAPIController.postTotalRequests() } } From 993b8f6a71bc2050b2cd373ab7672fb2b447a4ec Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Wed, 13 Oct 2021 16:39:01 +0200 Subject: [PATCH 07/11] Small improvements - Improve formatting - Fix some typos - Mini code improvements --- .../thecitadelofricks/controller/CacheController.kt | 12 ++++++------ .../controller/StartupController.kt | 3 ++- .../thecitadelofricks/controller/StatusController.kt | 2 +- .../thecitadelofricks/hsoparser/TimetableParser.kt | 5 ++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt index 24d4ddd..879098b 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt @@ -48,11 +48,11 @@ class CacheController { scheduledUpdates() } - companion object{ + companion object { private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) var courseList = CoursesList(CoursesMeta(), sortedMapOf()) - var mensaMenu = MensaMenu(MensaMeta(0,""), MensaWeek(), MensaWeek()) + var mensaMenu = MensaMenu(MensaMeta(0, ""), MensaWeek(), MensaWeek()) var timetableList = HashMap() // this list contains all timetables /** @@ -68,7 +68,7 @@ class CacheController { val currentTime = System.currentTimeMillis() / 1000 val timetableLink = "https://mosad.xyz" val weekNumberYear = 0 - val instr = javaClass.getResourceAsStream("/html/Timetable_normal-week.html") + val instr = CacheController::class.java.getResourceAsStream("/html/Timetable_normal-week.html") val timetableParser = TimetableParser(htmlDoc = Jsoup.parse(instr, "UTF-8", "https://www.hs-offenburg.de/")) @@ -151,7 +151,7 @@ class CacheController { /** * this function updates the courseList - * during the update process the old data will be returned for a API request + * during the update process the old data will be returned for an API request */ private fun asyncUpdateCourseList() = GlobalScope.launch { CourseListParser().getCourseLinks(StartupController.courseListURL)?.let { @@ -167,7 +167,7 @@ class CacheController { /** * this function updates the mensa menu list - * during the update process the old data will be returned for a API request + * during the update process the old data will be returned for an API request */ private fun asyncUpdateMensa() = GlobalScope.launch { val mensaCurrentWeek = MensaParser().getMensaMenu(StartupController.mensaMenuURL) @@ -183,7 +183,7 @@ class CacheController { /** * this function updates all existing timetables - * during the update process the old data will be returned for a API request + * during the update process the old data will be returned for an API request * a FixedThreadPool is used to make parallel requests for faster updates */ private fun asyncUpdateTimetables() = GlobalScope.launch { diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StartupController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StartupController.kt index d26160f..5ec69fc 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StartupController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StartupController.kt @@ -135,7 +135,8 @@ class StartupController { try { val timetableObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject val timetable = Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass) - CacheController.timetableList.put("${timetable.meta.courseName}-${timetable.meta.weekIndex}", timetable) + CacheController.timetableList["${timetable.meta.courseName}-${timetable.meta.weekIndex}"] = + timetable } catch (ex: Exception) { logger.error("error while reading cache", ex) } finally { diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt index 09f6da7..e32e547 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt @@ -62,7 +62,7 @@ class StatusController { } fun updateTimetableRequests(courseName: String) { - timetableRequests[courseName] = timetableRequests.getOrPut(courseName) {0} + 1 + timetableRequests[courseName] = (timetableRequests[courseName] ?: 0) + 1 totalRequests++ } diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt index c544530..39e386c 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt @@ -72,7 +72,7 @@ class TimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) { var lessonIndexDay = 0 // the index of the lesson per timeslot // elements are now all lessons, including empty ones - row.select("td.lastcol, td[style]").forEach {element -> + row.select("td.lastcol, td[style]").forEach { element -> // if there is a lecture with rowspan="2", we need to shift everything by one to the left. This is stupid and ugly there needs to bee an API if ((sDay > -1 && sRow > -1) && (sDay == day && ((sRow + 1) == rowIndex))) { @@ -115,8 +115,7 @@ class TimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) { lessonIndexDay++ - if (element.hasClass("lastcol")) - { + if (element.hasClass("lastcol")) { day++ lessonIndexDay = 0 } From fb6291792dd86a700c6a88e75db26c1d7427d69c Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Thu, 14 Oct 2021 00:32:28 +0200 Subject: [PATCH 08/11] Use ConcurrentHashMap for timetableList Multiple requests may be processed at the same time and could otherwise cause problems (concurrent write operations) --- .../org/mosad/thecitadelofricks/controller/CacheController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt index 879098b..e95c481 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt @@ -33,9 +33,9 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.* import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import kotlin.collections.ArrayList -import kotlin.collections.HashMap import kotlin.collections.HashSet import kotlin.concurrent.scheduleAtFixedRate import kotlin.time.Duration @@ -53,7 +53,7 @@ class CacheController { var courseList = CoursesList(CoursesMeta(), sortedMapOf()) var mensaMenu = MensaMenu(MensaMeta(0, ""), MensaWeek(), MensaWeek()) - var timetableList = HashMap() // this list contains all timetables + var timetableList = ConcurrentHashMap() // this list contains all timetables /** * get a timetable, since they may not be cached, we need to make sure it's cached, otherwise download From 8e3af696e06a5e1f74f5ba951a9aee14e9e94f8d Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Fri, 15 Oct 2021 20:07:58 +0200 Subject: [PATCH 09/11] Limit sending timetable requests in parallel to 3 Otherwise, the requests may fail (from my experience). Also we don't want to look suspicious (flooding their server with tons of requests at the same time). --- .../hsoparser/TimetableParser.kt | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt index 39e386c..9d691d5 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt @@ -22,6 +22,8 @@ package org.mosad.thecitadelofricks.hsoparser +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Semaphore import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.mosad.thecitadelofricks.Lesson @@ -36,16 +38,26 @@ class TimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) { private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableParser::class.java) private val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") + companion object { + val semaphore = Semaphore(3, 0) + } + private val htmlDoc: Document? = htmlDoc ?: if (timetableURL == null) { null } else { - try { - Jsoup.connect(timetableURL).get() - } catch (gex: Exception) { - logger.error("general TimetableParser error", gex) - null + runBlocking { + try { + // Only allow sending a limited amount of requests at the same time + semaphore.acquire() + Jsoup.connect(timetableURL).get() + } catch (gex: Exception) { + logger.error("general TimetableParser error", gex) + null + } finally { + semaphore.release() + } } } From ca8efdaa856b697c29600c85c7b88cbac2dd802b Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Sat, 16 Oct 2021 01:08:52 +0200 Subject: [PATCH 10/11] Only add timetable to cache on success --- .../org/mosad/thecitadelofricks/controller/CacheController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt index e95c481..f438870 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt @@ -106,7 +106,7 @@ class CacheController { weekNumberYear ?: 0, timetableLink ), timetable ?: TimetableWeek() - ).also { timetableList[key] = it } + ).also { if (timetable != null) timetableList[key] = it } } } From 5ba9dfc263f3caf5046eca04baf4c5b664c32b4f Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Sat, 23 Oct 2021 13:50:09 +0200 Subject: [PATCH 11/11] Better null checks --- .../hsoparser/TimetableParser.kt | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt index 9d691d5..3fc6c4d 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableParser.kt @@ -42,37 +42,29 @@ class TimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) { val semaphore = Semaphore(3, 0) } - private val htmlDoc: Document? = - htmlDoc - ?: if (timetableURL == null) { + private val htmlDoc: Document? = htmlDoc ?: timetableURL?.let { + runBlocking { + try { + // Only allow sending a limited amount of requests at the same time + semaphore.acquire() + Jsoup.connect(timetableURL).get() + } catch (gex: Exception) { + logger.error("general TimetableParser error", gex) null - } else { - runBlocking { - try { - // Only allow sending a limited amount of requests at the same time - semaphore.acquire() - Jsoup.connect(timetableURL).get() - } catch (gex: Exception) { - logger.error("general TimetableParser error", gex) - null - } finally { - semaphore.release() - } - } + } finally { + semaphore.release() } + } + } /** * parse the timetable from the previously given url * the timetable is organised per row not per column; * Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on */ - fun parseTimeTable(): TimetableWeek? { - if (htmlDoc == null) { - return null - } - + fun parseTimeTable(): TimetableWeek? = htmlDoc?.let { val timetableWeek = TimetableWeek() - val rows = htmlDoc.select("table.timetable").select("tr[scope=\"row\"]") + val rows = it.select("table.timetable").select("tr[scope=\"row\"]") var sDay = -1 var sRow = -1 @@ -141,12 +133,8 @@ class TimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) { /** * parse the week number of the year for the timetable */ - fun parseWeekNumberYear(): Int? { - if (htmlDoc == null) { - return null - } - - return htmlDoc.select("h1.timetable-caption").text().substringAfter("- ") + fun parseWeekNumberYear(): Int? = htmlDoc?.let { + it.select("h1.timetable-caption").text().substringAfter("- ") .substringBefore(".").replace(" ", "").toInt() }