diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/APIController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/APIController.kt index 52c941c..06031ad 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/APIController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/APIController.kt @@ -26,12 +26,16 @@ import org.mosad.thecitadelofricks.controller.CacheController import org.mosad.thecitadelofricks.controller.CacheController.Companion.courseList import org.mosad.thecitadelofricks.controller.CacheController.Companion.getLesson import org.mosad.thecitadelofricks.controller.CacheController.Companion.getLessonSubjectList +import org.mosad.thecitadelofricks.controller.CacheController.Companion.getRoomSchedule import org.mosad.thecitadelofricks.controller.CacheController.Companion.getTimetable import org.mosad.thecitadelofricks.controller.CacheController.Companion.mensaMenu +import org.mosad.thecitadelofricks.controller.CacheController.Companion.roomList import org.mosad.thecitadelofricks.controller.StartupController import org.mosad.thecitadelofricks.controller.StatusController.Companion.getStatus import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateCourseListRequests import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateMensaMenuRequests +import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateRoomListRequests +import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateRoomScheduleRequests import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateTimetableRequests import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -48,7 +52,7 @@ class APIController { private val logger: Logger = LoggerFactory.getLogger(APIController::class.java) companion object { - const val apiVersion = "1.3.0" + const val apiVersion = "1.4.0" const val softwareVersion = "1.3.1" val startTime = System.currentTimeMillis() / 1000 } @@ -104,6 +108,23 @@ class APIController { return getLesson(courseName, lessonSubject, week) } + @RequestMapping("/roomList") + fun roomList(): RoomsListRet { + logger.info("roomList request at ${LocalDateTime.now()}!") + updateRoomListRequests() + return RoomsListRet(roomList.meta, ArrayList(roomList.rooms.values)) + } + + @RequestMapping("/roomSchedule") + fun roomSchedule( + @RequestParam(value = "room", defaultValue = "B040") roomName: String, + @RequestParam(value = "week", defaultValue = "0") week: Int + ): RoomScheduleWeekRet { + logger.info("roomSchedule request at ${LocalDateTime.now()}!") + updateRoomScheduleRequests(roomName) + return getRoomSchedule(roomName, week) + } + @RequestMapping("/status") fun status(): Status { logger.info("status request at ${LocalDateTime.now()}!") diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/DataTypes.kt b/src/main/kotlin/org/mosad/thecitadelofricks/DataTypes.kt index dbc5225..490f6f7 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/DataTypes.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/DataTypes.kt @@ -65,6 +65,18 @@ data class TimetableCourseMeta(var updateTime: Long = 0, val courseName: String data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeek = TimetableWeek()) +// data classes for the room occupancy part +data class Room(val roomName: String, val roomLink: String) + +data class RoomsMeta(val updateTime: Long = 0, val totalRooms: Int = 0) + +data class RoomsList(val meta: RoomsMeta = RoomsMeta(), val rooms: SortedMap) +data class RoomsListRet(val meta: RoomsMeta = RoomsMeta(), val rooms: ArrayList = ArrayList()) + +data class RoomScheduleMeta(var updateTime: Long = 0, val roomName: String = "", val weekIndex: Int = 0, var weekNumberYear: Int = 0, var year: Int = 0, val link: String = "") + +data class RoomScheduleWeekRet(val meta: RoomScheduleMeta = RoomScheduleMeta(), var timetable: TimetableWeek = TimetableWeek()) + // data classes for the status part @@ -79,6 +91,10 @@ data class Status( val timetableRequests: HashMap, val timetableListSize: Int, val coursesLastUpdate: Date, + val roomListRequests: Int, + val roomScheduleRequests: HashMap, + val roomScheduleListSize: Int, + val roomsLastUpdate: Date, val mensaLastUpdate: Date, val hsoResponseCode: Int, val swfrResponseCode: Int diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt index 74c3d0b..5640a72 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/CacheController.kt @@ -28,6 +28,7 @@ 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.RoomListParser import org.mosad.thecitadelofricks.hsoparser.TimetableParser import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -55,6 +56,8 @@ class CacheController { var courseList = CoursesList(CoursesMeta(), sortedMapOf()) var mensaMenu = MensaMenu(MensaMeta(0, ""), MensaWeek(), MensaWeek()) var timetableList = ConcurrentHashMap() // this list contains all timetables + var roomList = RoomsList(RoomsMeta(), sortedMapOf()) + var roomScheduleList = ConcurrentHashMap() // this list contains all room schedules /** * get a timetable, since they may not be cached, we need to make sure it's cached, otherwise download @@ -151,6 +154,40 @@ class CacheController { return lessonList } + /** + * Get a room schedule. + * Since they may not be cached, we need to make sure it's cached. Download the schedule if it is not cached. + * @param roomName the name of the room to be requested + * @param weekIndex request week number (current week = 0) + * @return room schedule of the room (Type: [RoomScheduleWeekRet]) + */ + fun getRoomSchedule(roomName: String, weekIndex: Int): RoomScheduleWeekRet { + val key = "$roomName-$weekIndex" + return if (roomScheduleList.containsKey(key)) { + roomScheduleList[key]!! + } else { + val roomScheduleLink = roomList.rooms[roomName] + ?.roomLink + ?.replace("week=0", "week=$weekIndex") ?: "" + val currentTime = System.currentTimeMillis() / 1000 + + val roomScheduleParser = TimetableParser(roomScheduleLink) + val calendarWeek = roomScheduleParser.parseCalendarWeek() + val roomSchedule = roomScheduleParser.parseTimeTable() + + RoomScheduleWeekRet( + RoomScheduleMeta( + currentTime, + roomName, + weekIndex, + calendarWeek?.week ?: 0, + calendarWeek?.year ?: 0, + roomScheduleLink + ), roomSchedule ?: TimetableWeek() + ).also { if (roomSchedule != null) roomScheduleList[key] = it } + } + } + // private cache functions /** @@ -158,7 +195,7 @@ class CacheController { * during the update process the old data will be returned for an API request */ private fun asyncUpdateCourseList() = CoroutineScope(Dispatchers.IO).launch { - CourseListParser().getCourseLinks(StartupController.courseListURL)?.let { + CourseListParser().getLinks(StartupController.courseListURL)?.let { courseList = CoursesList(CoursesMeta(System.currentTimeMillis() / 1000, it.size), it.toSortedMap()) } @@ -169,6 +206,18 @@ class CacheController { logger.info("Updated courses successfully at ${Date(courseList.meta.updateTime * 1000)}") } + /** + * this function updates the roomList + * during the update process the old data will be returned for an API request + */ + private fun asyncUpdateRoomList() = CoroutineScope(Dispatchers.IO).launch { + RoomListParser().getLinks(StartupController.roomListURL)?.let { + roomList = RoomsList(RoomsMeta(System.currentTimeMillis() / 1000, it.size), it.toSortedMap()) + } + + logger.info("Updated room list successfully at ${Date(courseList.meta.updateTime * 1000)}") + } + /** * this function updates the mensa menu list * during the update process the old data will be returned for an API request @@ -236,6 +285,52 @@ class CacheController { } } + private fun asyncUpdateRoomSchedules() = CoroutineScope(Dispatchers.IO).launch { + logger.info("Updating ${roomScheduleList.size} room schedules ...") + + // create a new ThreadPool with 5 threads + val executor = Executors.newFixedThreadPool(5) + + try { + roomScheduleList.forEach { roomSchedule -> + executor.execute { + val roomScheduleParser = TimetableParser(roomSchedule.value.meta.link) + roomSchedule.value.timetable = roomScheduleParser.parseTimeTable() ?: return@execute + roomScheduleParser.parseCalendarWeek()?.also { + roomSchedule.value.meta.weekNumberYear = it.week + roomSchedule.value.meta.year = it.year + } ?: return@execute + roomSchedule.value.meta.updateTime = System.currentTimeMillis() / 1000 + + saveRoomScheduleToCache(roomSchedule.value) // save the updated timetable to the cache directory + } + + } + } catch (ex: Exception) { + logger.error("Error while updating the room schedules", 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 roomSchedule a room schedule of the type [RoomScheduleWeekRet] + */ + private fun saveRoomScheduleToCache(roomSchedule: RoomScheduleWeekRet) { + val file = File(StartupController.dirTcorCache, "roomSchedule-${roomSchedule.meta.roomName}-${roomSchedule.meta.weekIndex}.json") + val writer = BufferedWriter(FileWriter(file)) + + try { + writer.write(Gson().toJson(roomSchedule)) + } 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 @@ -244,9 +339,11 @@ class CacheController { // get all course links on startup, make sure there are course links val jobCourseUpdate = asyncUpdateCourseList() val jobMensa = asyncUpdateMensa() + val jobRoomListUpdate = asyncUpdateRoomList() jobCourseUpdate.join() jobMensa.join() + jobRoomListUpdate.join() logger.info("Initial updates successful") } @@ -269,15 +366,21 @@ class CacheController { val initDelay3h = calcInitDelay(duration3h) val initDelay1h = calcInitDelay(duration1h) - // update courseList every 24 hours (time in ms) + // update courseList and roomList every 24 hours (time in ms) Timer().scheduleAtFixedRate(initDelay24h, duration24h) { asyncUpdateCourseList() } + Timer().scheduleAtFixedRate(initDelay24h, duration24h) { + asyncUpdateRoomList() + } - // update all already existing timetables every 3 hours (time in ms) + // update all already existing timetables and room schedules every 3 hours (time in ms) Timer().scheduleAtFixedRate(initDelay3h, duration3h) { asyncUpdateTimetables() } + Timer().scheduleAtFixedRate(initDelay3h, duration3h) { + asyncUpdateRoomSchedules() + } // update mensa menu every hour (time in ms) Timer().scheduleAtFixedRate(initDelay1h, duration1h) { diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StartupController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StartupController.kt index 31e1bad..1f9557b 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StartupController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StartupController.kt @@ -44,6 +44,7 @@ class StartupController { var cachetAPIKey = "0" var cachetBaseURL = "https://status.mosad.xyz" var courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/" + val roomListURL = "https://www.hs-offenburg.de/die-hochschule/organisation/infos-services/raumbelegungen" var mensaMenuURL = "https://www.swfr.de/essen/mensen-cafes-speiseplaene/mensa-offenburg" var mensaName = "Offenburg" } diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt index e32e547..dd72bd1 100644 --- a/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt +++ b/src/main/kotlin/org/mosad/thecitadelofricks/controller/StatusController.kt @@ -47,6 +47,10 @@ class StatusController { private set var timetableRequests = HashMap() private set + var roomListRequests = 0 + private set + var roomScheduleRequests = HashMap() + private set /** * if a mensamenu/courseList/timetable is requested update the specific and total request count @@ -66,6 +70,16 @@ class StatusController { totalRequests++ } + fun updateRoomListRequests() { + roomListRequests++ + totalRequests++ + } + + fun updateRoomScheduleRequests(roomName: String) { + roomScheduleRequests[roomName] = (roomScheduleRequests[roomName] ?: 0) + 1 + totalRequests++ + } + fun getStatus(): Status { val currentTime = System.currentTimeMillis() / 1000 val minutes = (currentTime - startTime) % 3600 / 60 @@ -103,6 +117,10 @@ class StatusController { timetableRequests, CacheController.timetableList.size, Date(CacheController.courseList.meta.updateTime * 1000), + roomListRequests, + roomScheduleRequests, + CacheController.roomScheduleList.size, + Date(CacheController.roomList.meta.updateTime * 1000), Date(CacheController.mensaMenu.meta.updateTime * 1000), hsoCode, swfrCode diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/CourseListParser.kt b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/CourseListParser.kt deleted file mode 100644 index 6291d4c..0000000 --- a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/CourseListParser.kt +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 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.hsoparser - -import org.jsoup.Jsoup -import org.mosad.thecitadelofricks.Course -import org.slf4j.LoggerFactory -import java.net.SocketTimeoutException - -class CourseListParser { - - private var logger: org.slf4j.Logger = LoggerFactory.getLogger(CourseListParser::class.java) - - /** - * return a list of all courses at courseListURL - * @param courseListURL the url to the course list page - * @return a ArrayList with all courses or null if the request was not successful - */ - fun getCourseLinks(courseListURL: String): HashMap? { - val courseLinkList = HashMap() - try { - val courseHTML = Jsoup.connect(courseListURL).get() - - courseHTML.select("ul.index-group").select("li.Class").select("a[href]").forEachIndexed { _, element -> - courseLinkList[element.text()] = Course( - element.text(), - element.attr("href").replace("http:", "https:") - ) - } - logger.info("successfully retrieved course List") - } catch (ex: SocketTimeoutException) { - logger.warn("timeout from hs-offenburg.de, updating on next attempt!") - return null - } catch (gex: Exception) { - logger.error("general CourseListParser error", gex) - return null - } - - return courseLinkList - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableLinkListParser.kt b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableLinkListParser.kt new file mode 100644 index 0000000..205ff8f --- /dev/null +++ b/src/main/kotlin/org/mosad/thecitadelofricks/hsoparser/TimetableLinkListParser.kt @@ -0,0 +1,86 @@ +/** + * 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.hsoparser + +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.mosad.thecitadelofricks.Course +import org.mosad.thecitadelofricks.Room +import org.slf4j.LoggerFactory +import java.net.SocketTimeoutException + +sealed class TimetableLinkListParser { + + private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableLinkListParser::class.java) + + abstract fun constructValue(key: String, link: String): T + + abstract val blacklist: List + + abstract val liClass: String + + /** + * return a list of all elements at listURL + * @param listURL the url to the list page + * @return a ArrayList with all links or null if the request was not successful + */ + fun getLinks(listURL: String): HashMap? { + val linkList = HashMap() + try { + val courseHTML = Jsoup.connect(listURL).get() + + courseHTML + .select("ul.index-group") + .select("li.$liClass") + .select("a[href]") + .filter{ it: Element -> !blacklist.contains(it.text()) } + .forEach { + linkList[it.text()] = constructValue( + it.text(), + it.attr("href").replace("http:", "https:") + ) + } + logger.info("successfully retrieved link List") + } catch (ex: SocketTimeoutException) { + logger.warn("timeout from hs-offenburg.de, updating on next attempt!") + return null + } catch (gex: Exception) { + logger.error("general TimetableLinkListParser error", gex) + return null + } + + return linkList + } +} + +class CourseListParser : TimetableLinkListParser() { + override fun constructValue(key: String, link: String) = Course(key, link) + override val blacklist = emptyList() + override val liClass = "Class" +} + +class RoomListParser : TimetableLinkListParser() { + override fun constructValue(key: String, link: String) = Room(key, link) + override val blacklist = listOf("STÜBER SYSTEMS") + override val liClass = "Room" +}