/** * TheCitadelofRicks * * Copyright 2019-2020 * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ package org.mosad.thecitadelofricks.controller import com.google.gson.Gson import kotlinx.coroutines.* import org.jsoup.Jsoup import org.mosad.thecitadelofricks.* import org.mosad.thecitadelofricks.hsoparser.CourseListParser import org.mosad.thecitadelofricks.hsoparser.MensaParser import org.mosad.thecitadelofricks.hsoparser.TimetableParser import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.* import java.util.* import java.util.concurrent.Executors import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.collections.HashSet import kotlin.concurrent.scheduleAtFixedRate class CacheController { init { initUpdates() scheduledUpdates() } companion object{ private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) var courseList = CoursesList() var mensaMenu = MensaMenu(MensaMeta(0,""), MensaWeek(), MensaWeek()) var timetableList = HashMap() // this list contains all timetables /** * get a timetable, since they may not be cached, we need to make sure it's cached, otherwise download * @param courseName the name of the course to be requested * @param weekIndex request week number (current week = 0) * @return timetable of the course (Type: [TimetableCourseWeek]) */ fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek = runBlocking { // TODO just for testing if (courseName == "TEST_A" || courseName == "TEST_B") { val currentTime = System.currentTimeMillis() / 1000 val timetableLink = "https://mosad.xyz" 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) return@runBlocking TimetableCourseWeek(TimetableCourseMeta(currentTime, courseName, weekIndex, weekNumberYear, timetableLink), timetableTest) } return@runBlocking timetableList.getOrPut("$courseName-$weekIndex") { 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) } ) timetableJobs.awaitAll() TimetableCourseWeek(TimetableCourseMeta(currentTime, courseName, weekIndex, weekNumberYear, timetableLink), timetable) } } /** * get every explicit lesson in a week for a selected course * @param courseName the name of the course to be requested * @param weekIndex request week number (current week = 0) * @return a HashSet of explicit lessons for one week */ fun getLessonSubjectList(courseName: String, weekIndex: Int): HashSet = runBlocking { val lessonSubjectList = ArrayList() // get every lesson subject for the given week val flatMap = getTimetable(courseName, weekIndex).timetable.days.flatMap { it.timeslots.asIterable() } flatMap.forEach { it.stream().filter { x -> x.lessonSubject.isNotEmpty() }.findAny().ifPresent { x -> lessonSubjectList.add(x.lessonSubject) } } return@runBlocking HashSet(lessonSubjectList) } /** * get every lesson of a subject in a week * @param courseName the name of the course to be requested * @param lessonSubject the lesson subject to be requested * @param weekIndex request week number (current week = 0) * @return a ArrayList<[Lesson]> of every lesson with lessonSubject for one week */ fun getLesson(courseName: String, lessonSubject: String, weekIndex: Int): ArrayList { val lessonList = ArrayList() // get all lessons from the weeks timetable val flatMap = getTimetable(courseName, weekIndex).timetable.days.flatMap { it.timeslots.asIterable() } flatMap.forEach { it.stream().filter { x -> x.lessonSubject.contains(lessonSubject) }.findAny().ifPresent { x -> lessonList.add(x) } } return lessonList } // private cache functions /** * this function updates the courseList * during the update process the old data will be returned for a API request */ private fun asyncUpdateCourseList() = GlobalScope.launch { CourseListParser().getCourseLinks(StartupController.courseListURL)?.let { courseList = CoursesList(CoursesMeta(System.currentTimeMillis() / 1000, it.size), it) } // TODO just for testing courseList.courses["TEST_A"] = Course("TEST_A", "https://mosad.xyz") courseList.courses["TEST_B"] = Course("TEST_B", "https://mosad.xyz") logger.info("Updated courses successful at ${Date(courseList.meta.updateTime * 1000)}") } /** * this function updates the mensa menu list * during the update process the old data will be returned for a API request */ private fun asyncUpdateMensa() = GlobalScope.launch { val mensaCurrentWeek = MensaParser().getMensaMenu(StartupController.mensaMenuURL) val mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(StartupController.mensaMenuURL)) // only update if we get valid data if (mensaCurrentWeek != null && mensaNextWeek != null) { mensaMenu = MensaMenu(MensaMeta(System.currentTimeMillis() / 1000, StartupController.mensaName), mensaCurrentWeek, mensaNextWeek) } logger.info("Updated mensamenu successful at ${Date(mensaMenu.meta.updateTime * 1000)}") } /** * this function updates all existing timetables * during the update process the old data will be returned for a API request * a FixedThreadPool is used to make parallel requests for faster updates */ private fun asyncUpdateTimetables() = GlobalScope.launch { logger.info("Updating ${timetableList.size} timetables ...") // create a new ThreadPool with 5 threads val executor = Executors.newFixedThreadPool(5) try { timetableList.forEach { timetableCourse -> executor.execute { timetableCourse.value.timetable = TimetableParser().getTimeTable(timetableCourse.value.meta.link) timetableCourse.value.meta.updateTime = System.currentTimeMillis() / 1000 saveTimetableToCache(timetableCourse.value) // save the updated timetable to the cache directory } } } catch (ex: Exception) { logger.error("Error while updating the timetables", ex) } finally { executor.shutdown() } } /** * save a timetable to the cache directory * this is only call on async updates, it is NOT call when first getting the timetable * @param timetable a timetable of the type [TimetableCourseWeek] */ private fun saveTimetableToCache(timetable: TimetableCourseWeek) { val file = File(StartupController.dirTcorCache, "timetable-${timetable.meta.courseName}-${timetable.meta.weekIndex}.json") val writer = BufferedWriter(FileWriter(file)) try { writer.write(Gson().toJson(timetable)) } catch (e: Exception) { logger.error("something went wrong while trying to write a cache file", e) } finally { writer.close() } } /** * before the APIController is up, get the data fist * runBlocking: otherwise the api would return no data to requests for a few seconds after startup */ private fun initUpdates() = runBlocking { // get all course links on startup, make sure there are course links val jobCourseUpdate = asyncUpdateCourseList() val jobMensa = asyncUpdateMensa() jobCourseUpdate.join() jobMensa.join() logger.info("Initial updates successful") } /** * 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! */ 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 // update courseList every 24 hours (time in ms) Timer().scheduleAtFixedRate(initDelay24h, 86400000) { asyncUpdateCourseList() } // update all already existing timetables every 3 hours (time in ms) Timer().scheduleAtFixedRate(initDelay3h, 10800000) { asyncUpdateTimetables() } // update courses every hour (time in ms) Timer().scheduleAtFixedRate(initDelay1h, 3600000) { asyncUpdateMensa() } // post to status.mosad.xyz every hour, if an API key is present if (StartupController.cachetAPIKey != "0") { Timer().scheduleAtFixedRate(initDelay1h, 3600000) { CachetAPIController.postTotalRequests() } } } } }