2019-10-20 11:52:44 +02:00
/ * *
* TheCitadelofRicks
*
2020-01-15 13:42:40 +01:00
* Copyright 2019 - 2020 < seil0 @mosad . xyz >
2019-10-20 11:52:44 +02:00
*
* 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
2019-11-14 18:43:00 +01:00
import com.google.gson.Gson
2020-06-06 23:07:23 +02:00
import kotlinx.coroutines.*
2020-08-06 16:31:05 +02:00
import org.jsoup.Jsoup
2019-10-20 11:52:44 +02:00
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
2020-08-06 17:27:49 +02:00
import java.io.*
2019-10-20 11:52:44 +02:00
import java.util.*
2021-10-14 00:32:28 +02:00
import java.util.concurrent.ConcurrentHashMap
2019-10-27 13:51:08 +01:00
import java.util.concurrent.Executors
2019-10-20 11:52:44 +02:00
import kotlin.collections.ArrayList
2019-10-27 13:51:08 +01:00
import kotlin.collections.HashSet
2019-10-20 11:52:44 +02:00
import kotlin.concurrent.scheduleAtFixedRate
2022-04-08 14:54:06 +02:00
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
2021-10-13 16:09:13 +02:00
import kotlin.time.ExperimentalTime
2019-10-20 11:52:44 +02:00
class CacheController {
init {
initUpdates ( )
scheduledUpdates ( )
}
2021-10-13 16:39:01 +02:00
companion object {
2019-10-20 11:52:44 +02:00
private val logger : Logger = LoggerFactory . getLogger ( CacheController :: class . java )
2020-08-16 19:56:13 +02:00
var courseList = CoursesList ( CoursesMeta ( ) , sortedMapOf ( ) )
2021-10-13 16:39:01 +02:00
var mensaMenu = MensaMenu ( MensaMeta ( 0 , " " ) , MensaWeek ( ) , MensaWeek ( ) )
2021-10-14 00:32:28 +02:00
var timetableList = ConcurrentHashMap < String , TimetableCourseWeek > ( ) // this list contains all timetables
2019-10-20 11:52:44 +02:00
/ * *
2020-03-02 22:34:10 +01:00
* get a timetable , since they may not be cached , we need to make sure it ' s cached , otherwise download
2019-10-20 11:52:44 +02:00
* @param courseName the name of the course to be requested
* @param weekIndex request week number ( current week = 0 )
2019-11-14 18:43:00 +01:00
* @return timetable of the course ( Type : [ TimetableCourseWeek ] )
2019-10-20 11:52:44 +02:00
* /
2021-10-13 15:57:26 +02:00
fun getTimetable ( courseName : String , weekIndex : Int ) : TimetableCourseWeek {
2020-08-06 16:31:05 +02:00
// TODO just for testing
if ( courseName == " TEST_A " || courseName == " TEST_B " ) {
val currentTime = System . currentTimeMillis ( ) / 1000
val timetableLink = " https://mosad.xyz "
val weekNumberYear = 0
2021-10-23 23:39:18 +02:00
val year = 0
2021-10-13 16:39:01 +02:00
val instr = CacheController :: class . java . getResourceAsStream ( " /html/Timetable_normal-week.html " )
2020-08-06 16:31:05 +02:00
2021-10-13 15:57:26 +02:00
val timetableParser =
2022-04-08 14:54:06 +02:00
TimetableParser ( htmlDoc = Jsoup . parse ( instr !! , " UTF-8 " , " https://www.hs-offenburg.de/ " ) )
2021-10-13 15:57:26 +02:00
val timetableTest = timetableParser . parseTimeTable ( )
return TimetableCourseWeek (
TimetableCourseMeta (
currentTime ,
courseName ,
weekIndex ,
weekNumberYear ,
2021-10-23 23:39:18 +02:00
year ,
2021-10-13 15:57:26 +02:00
timetableLink
) , timetableTest ?: TimetableWeek ( )
)
2020-08-06 16:31:05 +02:00
}
2021-10-13 15:57:26 +02:00
val key = " $courseName - $weekIndex "
2022-01-14 15:42:25 +01:00
return if ( timetableList . containsKey ( key ) ) {
2021-10-13 15:57:26 +02:00
timetableList [ key ] !!
} else {
2020-06-06 23:07:23 +02:00
val timetableLink = courseList . courses [ courseName ]
?. courseLink
?. replace ( " week=0 " , " week= $weekIndex " ) ?: " "
val currentTime = System . currentTimeMillis ( ) / 1000
2021-10-13 15:57:26 +02:00
val timetableParser = TimetableParser ( timetableLink )
2021-10-23 23:39:18 +02:00
val calendarWeek = timetableParser . parseCalendarWeek ( )
2021-10-13 15:57:26 +02:00
val timetable = timetableParser . parseTimeTable ( )
TimetableCourseWeek (
TimetableCourseMeta (
currentTime ,
courseName ,
weekIndex ,
2021-10-23 23:39:18 +02:00
calendarWeek ?. week ?: 0 ,
calendarWeek ?. year ?: 0 ,
2021-10-13 15:57:26 +02:00
timetableLink
) , timetable ?: TimetableWeek ( )
2021-10-16 01:08:52 +02:00
) . also { if ( timetable != null ) timetableList [ key ] = it }
2019-10-20 11:52:44 +02:00
}
}
/ * *
2020-03-02 22:34:10 +01:00
* get every explicit lesson in a week for a selected course
2019-10-20 11:52:44 +02:00
* @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 < String > = runBlocking {
val lessonSubjectList = ArrayList < String > ( )
// 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 )
}
/ * *
2020-01-15 13:42:40 +01:00
* get every lesson of a subject in a week
2019-10-20 11:52:44 +02:00
* @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 )
2019-11-14 18:43:00 +01:00
* @return a ArrayList < [ Lesson ] > of every lesson with lessonSubject for one week
2019-10-20 11:52:44 +02:00
* /
fun getLesson ( courseName : String , lessonSubject : String , weekIndex : Int ) : ArrayList < Lesson > {
val lessonList = ArrayList < Lesson > ( )
// get all lessons from the weeks timetable
val flatMap = getTimetable ( courseName , weekIndex ) . timetable . days . flatMap { it . timeslots . asIterable ( ) }
flatMap . forEach {
2019-12-30 01:36:21 +01:00
it . stream ( ) . filter { x -> x . lessonSubject . contains ( lessonSubject ) } . findAny ( ) . ifPresent { x -> lessonList . add ( x ) }
2019-10-20 11:52:44 +02:00
}
return lessonList
}
2020-02-16 17:17:39 +01:00
// private cache functions
2019-10-20 11:52:44 +02:00
2020-02-16 17:17:39 +01:00
/ * *
* this function updates the courseList
2021-10-13 16:39:01 +02:00
* during the update process the old data will be returned for an API request
2020-02-16 17:17:39 +01:00
* /
2021-10-31 22:20:14 +01:00
private fun asyncUpdateCourseList ( ) = CoroutineScope ( Dispatchers . IO ) . launch {
2020-02-16 17:17:39 +01:00
CourseListParser ( ) . getCourseLinks ( StartupController . courseListURL ) ?. let {
2020-08-16 19:56:13 +02:00
courseList = CoursesList ( CoursesMeta ( System . currentTimeMillis ( ) / 1000 , it . size ) , it . toSortedMap ( ) )
2020-02-16 17:17:39 +01:00
}
2019-10-20 11:52:44 +02:00
2020-08-06 16:31:05 +02:00
// TODO just for testing
courseList . courses [ " TEST_A " ] = Course ( " TEST_A " , " https://mosad.xyz " )
courseList . courses [ " TEST_B " ] = Course ( " TEST_B " , " https://mosad.xyz " )
2020-02-16 17:17:39 +01:00
logger . info ( " Updated courses successful at ${Date(courseList.meta.updateTime * 1000)} " )
2019-10-20 11:52:44 +02:00
}
2020-02-16 17:17:39 +01:00
/ * *
* this function updates the mensa menu list
2021-10-13 16:39:01 +02:00
* during the update process the old data will be returned for an API request
2020-02-16 17:17:39 +01:00
* /
2021-10-31 22:20:14 +01:00
private fun asyncUpdateMensa ( ) = CoroutineScope ( Dispatchers . IO ) . launch {
2020-02-16 17:17:39 +01:00
val mensaCurrentWeek = MensaParser ( ) . getMensaMenu ( StartupController . mensaMenuURL )
val mensaNextWeek = MensaParser ( ) . getMensaMenu ( MensaParser ( ) . getMenuLinkNextWeek ( StartupController . mensaMenuURL ) )
2019-10-20 11:52:44 +02:00
2020-02-16 17:17:39 +01:00
// only update if we get valid data
if ( mensaCurrentWeek != null && mensaNextWeek != null ) {
mensaMenu = MensaMenu ( MensaMeta ( System . currentTimeMillis ( ) / 1000 , StartupController . mensaName ) , mensaCurrentWeek , mensaNextWeek )
}
2019-10-27 13:51:08 +01:00
2020-02-16 17:17:39 +01:00
logger . info ( " Updated mensamenu successful at ${Date(mensaMenu.meta.updateTime * 1000)} " )
}
2019-10-27 13:51:08 +01:00
2020-02-16 17:17:39 +01:00
/ * *
* this function updates all existing timetables
2021-10-13 16:39:01 +02:00
* during the update process the old data will be returned for an API request
2020-02-16 17:17:39 +01:00
* a FixedThreadPool is used to make parallel requests for faster updates
* /
2021-10-31 22:20:14 +01:00
private fun asyncUpdateTimetables ( ) = CoroutineScope ( Dispatchers . IO ) . launch {
2020-02-16 17:17:39 +01:00
logger . info ( " Updating ${timetableList.size} timetables ... " )
2019-11-14 18:43:00 +01:00
2020-02-16 17:17:39 +01:00
// create a new ThreadPool with 5 threads
val executor = Executors . newFixedThreadPool ( 5 )
2019-10-27 13:51:08 +01:00
2020-02-16 17:17:39 +01:00
try {
timetableList . forEach { timetableCourse ->
executor . execute {
2021-10-13 15:57:26 +02:00
val timetableParser = TimetableParser ( timetableCourse . value . meta . link )
timetableCourse . value . timetable = timetableParser . parseTimeTable ( ) ?: return @execute
2023-01-13 20:21:15 +01:00
timetableParser . parseCalendarWeek ( ) ?. also {
timetableCourse . value . meta . weekNumberYear = it . week
timetableCourse . value . meta . year = it . year
} ?: return @execute
2020-06-06 23:07:23 +02:00
timetableCourse . value . meta . updateTime = System . currentTimeMillis ( ) / 1000
2019-10-20 11:52:44 +02:00
2020-06-06 23:07:23 +02:00
saveTimetableToCache ( timetableCourse . value ) // save the updated timetable to the cache directory
2020-02-16 17:17:39 +01:00
}
2019-11-14 18:43:00 +01:00
2020-02-16 17:17:39 +01:00
}
} catch ( ex : Exception ) {
logger . error ( " Error while updating the timetables " , ex )
} finally {
executor . shutdown ( )
2019-10-20 11:52:44 +02:00
}
}
2020-02-16 17:17:39 +01:00
/ * *
* 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 ) )
2020-06-06 20:53:23 +02:00
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 ( )
}
2019-10-20 11:52:44 +02:00
}
2020-02-16 17:17:39 +01:00
/ * *
* 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 ( )
2019-10-20 11:52:44 +02:00
2020-02-16 17:17:39 +01:00
jobCourseUpdate . join ( )
jobMensa . join ( )
2019-10-20 11:52:44 +02:00
2020-02-16 17:17:39 +01:00
logger . info ( " Initial updates successful " )
2019-10-20 11:52:44 +02:00
}
2020-02-16 17:17:39 +01:00
/ * *
* update the CourseList every 24 h , the Timetables every 3 h and the Mensa Menu every hour
* doesn ' t account the change between winter and summer time !
* /
private fun scheduledUpdates ( ) {
val currentTime = System . currentTimeMillis ( )
2021-10-13 16:09:13 +02:00
2022-04-08 14:54:06 +02:00
val duration24h = 24. hours . inWholeMilliseconds
val duration3h = 3. hours . inWholeMilliseconds
val duration1h = 1. hours . inWholeMilliseconds
val duration1m = 1. minutes . inWholeMilliseconds
2021-10-13 16:09:13 +02:00
// 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 )
2020-02-16 17:17:39 +01:00
// update courseList every 24 hours (time in ms)
2021-10-13 16:09:13 +02:00
Timer ( ) . scheduleAtFixedRate ( initDelay24h , duration24h ) {
2020-02-16 17:17:39 +01:00
asyncUpdateCourseList ( )
}
2019-10-20 11:52:44 +02:00
2020-02-16 17:17:39 +01:00
// update all already existing timetables every 3 hours (time in ms)
2021-10-13 16:09:13 +02:00
Timer ( ) . scheduleAtFixedRate ( initDelay3h , duration3h ) {
2020-02-16 17:17:39 +01:00
asyncUpdateTimetables ( )
}
2019-10-20 11:52:44 +02:00
2023-08-24 23:26:57 +02:00
// update mensa menu every hour (time in ms)
2021-10-13 16:09:13 +02:00
Timer ( ) . scheduleAtFixedRate ( initDelay1h , duration1h ) {
2020-02-16 17:17:39 +01:00
asyncUpdateMensa ( )
}
// post to status.mosad.xyz every hour, if an API key is present
if ( StartupController . cachetAPIKey != " 0 " ) {
2021-10-13 16:09:13 +02:00
Timer ( ) . scheduleAtFixedRate ( initDelay1h , duration1h ) {
2020-02-16 17:17:39 +01:00
CachetAPIController . postTotalRequests ( )
}
2019-10-28 18:39:44 +01:00
}
2019-10-20 11:52:44 +02:00
}
2020-02-16 17:17:39 +01:00
}
2019-10-20 11:52:44 +02:00
}