/** * TheCitadelofRicks * * Copyright 2019 * * 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 import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking 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 org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import java.net.HttpURLConnection import java.net.URL import java.time.LocalDateTime import java.util.* import kotlin.collections.ArrayList import kotlin.concurrent.scheduleAtFixedRate @RestController class APIController { // Controller stuff var logger: Logger = LoggerFactory.getLogger(APIController::class.java) private val startTime = System.currentTimeMillis() / 1000 private val softwareVersion = "1.0.2" private val apiVersion = "1.0.1" // hso parser links (hardcoded) private val mensaLink = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" private val mensaName = "Offenburg" // cache objects private var coursesLinkList = ArrayList() private var coursesLastUpdate: Long = 0 private var timetableList = ArrayList() // this list contains all timetables private var mensaCurrentWeek = MensaWeek() private var mensaNextWeek = MensaWeek() private var mensaLastUpdate: Long = 0 init { initUpdates() // without this 5-10 seconds after startup the response will be empty val currentTime = System.currentTimeMillis() val delay24h = (86400000 - ((currentTime + 3600000) % 86400000)) + 60000 val delay3h = (10800000 - ((currentTime + 3600000) % 10800000)) + 60000 // update courses every 24 hours (time in ms) Timer().scheduleAtFixedRate(delay24h, 86400000) { asyncUpdateCourses() } // update courses every 3 hours (time in ms) Timer().scheduleAtFixedRate(delay3h, 10800000) { asyncUpdateMensa() } // update all already existing timetables every 3 hours (time in ms) Timer().scheduleAtFixedRate(delay3h, 10800000) { asyncUpdateTimetables() } } @RequestMapping("/courses") fun courses(): CoursesList { logger.info("courses request at ${LocalDateTime.now()}!") return CoursesList(CoursesMeta(coursesLastUpdate, coursesLinkList.size), coursesLinkList) } @RequestMapping("/mensamenu") fun mensamenu(): Mensa { logger.info("mensamenu request at ${LocalDateTime.now()}!") return Mensa(MensaMeta(mensaLastUpdate, mensaName), mensaCurrentWeek, mensaNextWeek) } @RequestMapping("/timetable") fun timetable( @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, @RequestParam(value = "week", defaultValue = "0") week: Int ): TimetableCourseWeek { logger.info("timetable request at ${LocalDateTime.now()}!") checkTimetableCourse(courseName, week) // check if we need to update and perform the update if so return timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.week == week }.findAny().orElse(null) } @RequestMapping("/status") fun status(): Status { val currentTime = System.currentTimeMillis() / 1000 val minutes = (currentTime - startTime) % 3600 / 60 val hours = (currentTime - startTime) % 86400 / 3600 val days = (currentTime - startTime) / 86400 var hsoCode = 999 var swfrCode = 999 logger.info("status request at ${LocalDateTime.now()}!") try { val hsoURL = URL("https://www.hs-offenburg.de/") var connection = hsoURL.openConnection() as HttpURLConnection connection.requestMethod = "HEAD" hsoCode = connection.responseCode val swfrURL = URL("https://www.swfr.de/") connection = swfrURL.openConnection() as HttpURLConnection swfrCode = connection.responseCode } catch (e: Exception) { logger.error("Error while fetching url response codes!", e) } return Status( LocalDateTime.now(), "$days days, $hours:$minutes", apiVersion, softwareVersion, Date(coursesLastUpdate * 1000), Date(mensaLastUpdate * 1000), hsoCode, swfrCode ) } /** * this function updates the courses list * during the update process the old data will be returned for a API request */ private fun asyncUpdateCourses() = GlobalScope.launch { coursesLinkList = CourseListParser().getCourseLinks() coursesLastUpdate = System.currentTimeMillis() / 1000 logger.info("updated courses successful at ${Date(coursesLastUpdate * 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 { mensaCurrentWeek = MensaParser().getMensaMenu(mensaLink) mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(mensaLink)) mensaLastUpdate = System.currentTimeMillis() / 1000 logger.info("updated mensamenu successful at ${Date(mensaLastUpdate * 1000)}") } /** * this function updates all existing timetables * during the update process the old data will be returned for a API request */ private fun asyncUpdateTimetables() = GlobalScope.launch { timetableList.forEach { timetableCourse -> val updateURL = timetableCourse.meta.link timetableCourse.timetable = TimetableParser().getTimeTable(updateURL) timetableCourse.meta.updateTime = System.currentTimeMillis() / 1000 } logger.info("updated ${timetableList.size} timetables successful!") } /** * this function checks if we need to update the timetable for a given course and if so does it * during the update process the old data will be returned for a API request * update if the last update was 6 hours ago */ private fun checkTimetableCourse(courseName: String, week: Int) = runBlocking { val currentTime = System.currentTimeMillis() / 1000 var timetable = TimetableWeek() // check if the timetable already exists and is up to date val result = timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.week == week }.findAny().orElse(null) when (result) { // there is no such course yet, create one null -> { val courseLink = coursesLinkList.stream().filter { x -> x.courseName == courseName }.findFirst().orElse(null).courseLink val timetableMeta = TimetableCourseMeta(currentTime, courseName, week, courseLink.replace("week=0","week=$week")) val jobTimetable = GlobalScope.async { timetable = TimetableParser().getTimeTable(timetableMeta.link) } jobTimetable.await() timetableList.add(TimetableCourseWeek(timetableMeta, timetable)) logger.info("added new timetable for $courseName, week $week") } } } private fun initUpdates() = runBlocking { // get all courses on startup val jobCourseUpdate = GlobalScope.async{ coursesLinkList = CourseListParser().getCourseLinks() coursesLastUpdate = System.currentTimeMillis() / 1000 } // get the current and next weeks mensa menus val jobCurrentMensa = GlobalScope.async{ mensaCurrentWeek = MensaParser().getMensaMenu(mensaLink) } val jobNextMensa = GlobalScope.async{ mensaCurrentWeek = MensaParser().getMensaMenu(mensaLink) mensaLastUpdate = System.currentTimeMillis() / 1000 } jobCourseUpdate.await() jobCurrentMensa.await() jobNextMensa.await() logger.info("init updates successful") } }