Add API for room occupancy
This commit is contained in:
		| @ -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()}!") | ||||
|  | ||||
| @ -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<String, Room>) | ||||
| data class RoomsListRet(val meta: RoomsMeta = RoomsMeta(), val rooms: ArrayList<Room> = 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<String, Int>, | ||||
|     val timetableListSize: Int, | ||||
|     val coursesLastUpdate: Date, | ||||
|     val roomListRequests: Int, | ||||
|     val roomScheduleRequests: HashMap<String, Int>, | ||||
|     val roomScheduleListSize: Int, | ||||
|     val roomsLastUpdate: Date, | ||||
|     val mensaLastUpdate: Date, | ||||
|     val hsoResponseCode: Int, | ||||
|     val swfrResponseCode: Int | ||||
|  | ||||
| @ -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<String, TimetableCourseWeek>() // this list contains all timetables | ||||
|         var roomList = RoomsList(RoomsMeta(), sortedMapOf()) | ||||
|         var roomScheduleList = ConcurrentHashMap<String, RoomScheduleWeekRet>() // 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) { | ||||
|  | ||||
| @ -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" | ||||
|     } | ||||
|  | ||||
| @ -47,6 +47,10 @@ class StatusController { | ||||
|             private set | ||||
|         var timetableRequests = HashMap<String, Int>() | ||||
|             private set | ||||
|         var roomListRequests = 0 | ||||
|             private set | ||||
|         var roomScheduleRequests = HashMap<String, Int>() | ||||
|             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 | ||||
|  | ||||
| @ -1,61 +0,0 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||
|  * | ||||
|  * 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<Course> with all courses or null if the request was not successful | ||||
|      */ | ||||
|     fun getCourseLinks(courseListURL: String): HashMap<String, Course>? { | ||||
|         val courseLinkList = HashMap<String, Course>() | ||||
|         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 | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,86 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||
|  * | ||||
|  * 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<T> { | ||||
|  | ||||
|     private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableLinkListParser::class.java) | ||||
|  | ||||
|     abstract fun constructValue(key: String, link: String): T | ||||
|  | ||||
|     abstract val blacklist: List<String> | ||||
|  | ||||
|     abstract val liClass: String | ||||
|  | ||||
|     /** | ||||
|      * return a list of all elements at listURL | ||||
|      * @param listURL the url to the list page | ||||
|      * @return a ArrayList<T> with all links or null if the request was not successful | ||||
|      */ | ||||
|     fun getLinks(listURL: String): HashMap<String, T>? { | ||||
|         val linkList = HashMap<String, T>() | ||||
|         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<Course>() { | ||||
|     override fun constructValue(key: String, link: String) = Course(key, link) | ||||
|     override val blacklist = emptyList<String>() | ||||
|     override val liClass = "Class" | ||||
| } | ||||
|  | ||||
| class RoomListParser : TimetableLinkListParser<Room>() { | ||||
|     override fun constructValue(key: String, link: String) = Room(key, link) | ||||
|     override val blacklist = listOf("STÜBER SYSTEMS") | ||||
|     override val liClass = "Room" | ||||
| } | ||||
		Reference in New Issue
	
	Block a user