Compare commits
	
		
			38 Commits
		
	
	
		
			1.2.2
			...
			24d0d9ac8c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 24d0d9ac8c | |||
| ca14a5d2f6 | |||
| 3d4e2f363d | |||
| 235cc7f773 | |||
| e7b5eff04b | |||
| 6f4cb9fd66 | |||
| 1f1374f112 | |||
| b783fb6c4f | |||
| 65dc8ea5d2 | |||
| 97efbd0877 | |||
| c62f576ace | |||
| 8ac78d29b1 | |||
| 024f2b04ce | |||
| bf71d62dc5 | |||
| 7dce2c6cfd | |||
| a1dc5656b8 | |||
| dd4c5259d2 | |||
| 884aab08ed | |||
| c64c8779e3 | |||
| 1d614a06c4 | |||
| 3f10c8afaa | |||
| 9de1e295dd | |||
| 6287d4582d | |||
| 7dfa0fc6c4 | |||
| a53b2b8fc1 | |||
| 36972c9322 | |||
| 7bf2920d17 | |||
| c30306c163 | |||
| 46c9a61124 | |||
| 36acf1a00a | |||
| f9029bf1c3 | |||
| fe72c02562 | |||
| 8d9fcd3d7c | |||
| ec7a0a7a64 | |||
| efd8f9f9f5 | |||
| e2dce9fab3 | |||
| bbac0d3688 | |||
| 6114077591 | 
| @ -1,9 +0,0 @@ | ||||
| kind: pipeline | ||||
| name: default | ||||
|  | ||||
| steps: | ||||
| - name: test | ||||
|   image: gradle:jdk11 | ||||
|   commands: | ||||
|   - gradle test | ||||
|  | ||||
							
								
								
									
										22
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| pipeline: | ||||
|   test: | ||||
|     image: gradle:jdk11 | ||||
|     commands: | ||||
|       - gradle test | ||||
|   build: | ||||
|     image: gradle:jdk11 | ||||
|     commands: | ||||
|       - gradle bootJar | ||||
|     when: | ||||
|       event: | ||||
|         - tag | ||||
|   docker: | ||||
|     image: plugins/docker | ||||
|     repo: mosadxyz/tcor | ||||
|     tags: latest | ||||
|     secrets: | ||||
|       - username: docker_username | ||||
|         password: docker_password | ||||
|     when: | ||||
|       event: | ||||
|         - tag | ||||
							
								
								
									
										8
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| FROM adoptopenjdk/openjdk11:alpine-jre | ||||
| RUN addgroup -S spring && adduser -S spring -G spring | ||||
| #RUN groupadd -r spring && useradd -r -g spring spring # for openjdk:xx builds | ||||
| USER spring:spring | ||||
| ARG JAR_FILE=build/libs/*.jar | ||||
| COPY ${JAR_FILE} thecitadelofricks.jar | ||||
| ENTRYPOINT ["java","-Djavax.net.ssl.trustStore=/tcor/cacerts", "-Djavax.net.ssl.trustStorePassword=changeit", "-jar","/thecitadelofricks.jar"] | ||||
| VOLUME /tcor | ||||
| @ -1,4 +1,5 @@ | ||||
| [](https://drone.mosad.xyz/Seil0/TheCitadelofRicks) | ||||
|  | ||||
| [](https://ci.mosad.xyz/Seil0/TheCitadelofRicks) | ||||
| [](https://git.mosad.xyz/Seil0/TheCitadelofRicks/releases) | ||||
| [](https://www.gnu.org/licenses/gpl-3.0) | ||||
| # TheCitadelofRicks | ||||
|  | ||||
							
								
								
									
										33
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								build.gradle
									
									
									
									
									
								
							| @ -1,26 +1,25 @@ | ||||
| plugins { | ||||
|     id 'org.jetbrains.kotlin.jvm' version '1.3.70' | ||||
|     id 'org.jetbrains.kotlin.plugin.spring' version '1.3.70' | ||||
|     id 'org.springframework.boot' version '2.2.5.RELEASE' | ||||
|     id 'io.spring.dependency-management' version '1.0.9.RELEASE' | ||||
|     id 'org.jetbrains.kotlin.jvm' version '1.5.31' | ||||
|     id 'org.jetbrains.kotlin.plugin.spring' version '1.5.31' | ||||
|     id 'org.springframework.boot' version '2.5.5' | ||||
|     id 'io.spring.dependency-management' version '1.0.11.RELEASE' | ||||
| } | ||||
|  | ||||
| group 'org.mosad' | ||||
| version '1.2.2' | ||||
| version '1.2.7' | ||||
|  | ||||
| repositories { | ||||
|     jcenter() | ||||
|     mavenCentral() | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation 'org.springframework.boot:spring-boot-starter-web' | ||||
|     implementation 'org.jetbrains.kotlin:kotlin-reflect' | ||||
|     implementation 'org.jetbrains.kotlin:kotlin-stdlib' | ||||
|     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' | ||||
|     implementation 'org.jsoup:jsoup:1.12.2' | ||||
|     implementation 'com.google.code.gson:gson:2.8.6' | ||||
|     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' | ||||
|     implementation 'org.jsoup:jsoup:1.14.3' | ||||
|     implementation 'com.google.code.gson:gson:2.8.8' | ||||
|  | ||||
|     testImplementation 'org.junit.jupiter:junit-jupiter:5.6.0' | ||||
|     testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' | ||||
| } | ||||
|  | ||||
| test { | ||||
| @ -31,9 +30,17 @@ test { | ||||
|     } | ||||
| } | ||||
|  | ||||
| def jvmTargetVersion = "11" | ||||
| compileKotlin { | ||||
|     kotlinOptions.jvmTarget = "11" | ||||
|     kotlinOptions.jvmTarget = jvmTargetVersion | ||||
|     kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" | ||||
| } | ||||
| compileJava { | ||||
|     targetCompatibility = jvmTargetVersion | ||||
| } | ||||
| compileTestKotlin { | ||||
|     kotlinOptions.jvmTarget = "11" | ||||
|     kotlinOptions.jvmTarget = jvmTargetVersion | ||||
| } | ||||
| compileTestJava { | ||||
|     targetCompatibility = jvmTargetVersion | ||||
| } | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
|  | ||||
							
								
								
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							| @ -82,6 +82,7 @@ esac | ||||
|  | ||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||||
|  | ||||
|  | ||||
| # Determine the Java command to use to start the JVM. | ||||
| if [ -n "$JAVA_HOME" ] ; then | ||||
|     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | ||||
| @ -129,6 +130,7 @@ fi | ||||
| if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then | ||||
|     APP_HOME=`cygpath --path --mixed "$APP_HOME"` | ||||
|     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` | ||||
|      | ||||
|     JAVACMD=`cygpath --unix "$JAVACMD"` | ||||
|  | ||||
|     # We build the pattern for arguments to be converted via cygpath | ||||
|  | ||||
							
								
								
									
										4
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							| @ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. | ||||
| set APP_BASE_NAME=%~n0 | ||||
| set APP_HOME=%DIRNAME% | ||||
|  | ||||
| @rem Resolve any "." and ".." in APP_HOME to make it shorter. | ||||
| for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi | ||||
|  | ||||
| @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||
| set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | ||||
|  | ||||
| @ -81,6 +84,7 @@ set CMD_LINE_ARGS=%* | ||||
|  | ||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||
|  | ||||
|  | ||||
| @rem Execute Gradle | ||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | ||||
|  | ||||
|  | ||||
| @ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestParam | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
| import java.time.LocalDateTime | ||||
| import java.util.* | ||||
| import kotlin.collections.ArrayList | ||||
|  | ||||
| @RestController | ||||
| class APIController { | ||||
| @ -47,8 +48,8 @@ class APIController { | ||||
|     private val logger: Logger = LoggerFactory.getLogger(APIController::class.java) | ||||
|  | ||||
|     companion object { | ||||
|         const val apiVersion = "1.1.4" | ||||
|         const val softwareVersion = "1.2.2" | ||||
|         const val apiVersion = "1.2.0" | ||||
|         const val softwareVersion = "1.2.8" | ||||
|         val startTime = System.currentTimeMillis() / 1000 | ||||
|     } | ||||
|  | ||||
| @ -57,18 +58,12 @@ class APIController { | ||||
|         CacheController() | ||||
|     } | ||||
|  | ||||
|     // TODO remove this with API version 2.0.0 | ||||
|     @Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()")) | ||||
|     @RequestMapping("/courses") | ||||
|     fun courses(): CoursesList { | ||||
|         return courseList() | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/courseList") | ||||
|     fun courseList(): CoursesList { | ||||
|     fun courseList(): CoursesListRet { | ||||
|         logger.info("courseList request at ${LocalDateTime.now()}!") | ||||
|         updateCourseListRequests() | ||||
|         return courseList | ||||
|  | ||||
|         return CoursesListRet(courseList.meta, ArrayList(courseList.courses.values)) | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/mensamenu") | ||||
| @ -80,7 +75,7 @@ class APIController { | ||||
|  | ||||
|     @RequestMapping("/timetable") | ||||
|     fun timetable( | ||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||
|     ): TimetableCourseWeek { | ||||
|         logger.info("timetable request at ${LocalDateTime.now()}!") | ||||
| @ -88,20 +83,20 @@ class APIController { | ||||
|         return getTimetable(courseName, week) | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/lessonSubjectList") | ||||
|     @RequestMapping("/subjectList") | ||||
|     fun lessonSubjectList( | ||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||
|     ): HashSet<String> { | ||||
|         logger.info("lessonSubjectList request at ${LocalDateTime.now()}!") | ||||
|         logger.info("subjectList request at ${LocalDateTime.now()}!") | ||||
|         updateTimetableRequests(courseName) | ||||
|         return getLessonSubjectList(courseName, week) | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/lessons") | ||||
|     fun lesson( | ||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "lessonSubject", defaultValue = "Mathematik 4") lessonSubject: String, | ||||
|         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "subject", defaultValue = "Mathematik 4") lessonSubject: String, | ||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||
|     ): ArrayList<Lesson> { | ||||
|         logger.info("lesson request at ${LocalDateTime.now()}!") | ||||
| @ -121,4 +116,25 @@ class APIController { | ||||
|         return 200 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Deprecated section | ||||
|      */ | ||||
|  | ||||
|     // TODO remove this with API version 2.0.0 | ||||
|     @Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()")) | ||||
|     @RequestMapping("/courses") | ||||
|     fun courses(): CoursesListRet { | ||||
|         return courseList() | ||||
|     } | ||||
|  | ||||
|     // TODO remove this with API version 2.0.0 | ||||
|     @Deprecated("the parameter courseName is deprecated please use course", ReplaceWith("timetable(courseName, week)")) | ||||
|     @RequestMapping("/timetable", params= ["courseName", "week"]) | ||||
|     fun timetableDep( | ||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||
|     ): TimetableCourseWeek { | ||||
|        return timetable(courseName, week) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -24,13 +24,15 @@ package org.mosad.thecitadelofricks | ||||
|  | ||||
| import java.time.LocalDateTime | ||||
| import java.util.* | ||||
| import kotlin.collections.HashMap | ||||
|  | ||||
| // data classes for the course part | ||||
| data class Course(val courseName: String, val courseLink: String) | ||||
|  | ||||
| data class CoursesMeta(val updateTime: Long, val totalCourses: Int) | ||||
| data class CoursesMeta(val updateTime: Long = 0, val totalCourses: Int = 0) | ||||
|  | ||||
| data class CoursesList(val meta: CoursesMeta, val courses: ArrayList<Course>) | ||||
| data class CoursesList(val meta: CoursesMeta = CoursesMeta(), val courses: SortedMap<String, Course>) | ||||
| data class CoursesListRet(val meta: CoursesMeta = CoursesMeta(), val courses: ArrayList<Course> = ArrayList()) | ||||
|  | ||||
| // data classes for the Mensa part | ||||
| data class Meal(val day: String, val heading: String, val parts: ArrayList<String>, val additives: String) | ||||
| @ -44,6 +46,9 @@ data class MensaMeta(val updateTime: Long, val mensaName: String) | ||||
| data class MensaMenu(val meta: MensaMeta, val currentWeek: MensaWeek, val nextWeek: MensaWeek) | ||||
|  | ||||
| // data classes for the timetable part | ||||
|  | ||||
| data class CalendarWeek(val week: Int, val year: Int) | ||||
|  | ||||
| data class Lesson( | ||||
|     val lessonID: String, | ||||
|     val lessonSubject: String, | ||||
| @ -56,13 +61,12 @@ data class TimetableDay(val timeslots: Array<ArrayList<Lesson>> = Array(6) { Arr | ||||
|  | ||||
| data class TimetableWeek(val days: Array<TimetableDay> = Array(6) { TimetableDay() }) | ||||
|  | ||||
| data class TimetableCourseMeta(var updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, val weekNumberYear: Int = 0, val link: String = "") | ||||
| data class TimetableCourseMeta(var updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, var weekNumberYear: Int = 0, val year: Int = 0, val link: String = "") | ||||
|  | ||||
| data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeek = TimetableWeek()) | ||||
|  | ||||
|  | ||||
| // data classes for the status part | ||||
| data class TimetableCounter(var courseName: String, var requests: Int) | ||||
|  | ||||
| data class Status( | ||||
|     val time: LocalDateTime, | ||||
| @ -72,7 +76,7 @@ data class Status( | ||||
|     val totalRequests: Int, | ||||
|     val mensaMenuRequests: Int, | ||||
|     val courseListRequests: Int, | ||||
|     val timetableRequests: ArrayList<TimetableCounter>, | ||||
|     val timetableRequests: HashMap<String, Int>, | ||||
|     val timetableListSize: Int, | ||||
|     val coursesLastUpdate: Date, | ||||
|     val mensaLastUpdate: Date, | ||||
|  | ||||
| @ -23,24 +23,23 @@ | ||||
| package org.mosad.thecitadelofricks.controller | ||||
|  | ||||
| import com.google.gson.Gson | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| 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.BufferedWriter | ||||
| import java.io.File | ||||
| import java.io.FileWriter | ||||
| import java.io.* | ||||
| import java.util.* | ||||
| import java.util.concurrent.ConcurrentHashMap | ||||
| import java.util.concurrent.Executors | ||||
| import kotlin.collections.ArrayList | ||||
| import kotlin.collections.HashSet | ||||
| import kotlin.concurrent.scheduleAtFixedRate | ||||
| import kotlin.time.Duration | ||||
| import kotlin.time.ExperimentalTime | ||||
|  | ||||
| class CacheController { | ||||
|  | ||||
| @ -52,9 +51,9 @@ class CacheController { | ||||
|     companion object { | ||||
|         private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) | ||||
|  | ||||
|         var courseList = CoursesList(CoursesMeta(0, 0), ArrayList()) | ||||
|         var courseList = CoursesList(CoursesMeta(), sortedMapOf()) | ||||
|         var mensaMenu = MensaMenu(MensaMeta(0, ""), MensaWeek(), MensaWeek()) | ||||
|         var timetableList = ArrayList<TimetableCourseWeek>() // this list contains all timetables | ||||
|         var timetableList = ConcurrentHashMap<String, TimetableCourseWeek>() // this list contains all timetables | ||||
|  | ||||
|         /** | ||||
|          * get a timetable, since they may not be cached, we need to make sure it's cached, otherwise download | ||||
| @ -62,31 +61,56 @@ class CacheController { | ||||
|          * @param weekIndex request week number (current week = 0) | ||||
|          * @return timetable of the course (Type: [TimetableCourseWeek]) | ||||
|          */ | ||||
|         fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek = runBlocking { | ||||
|         fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek { | ||||
|  | ||||
|             // TODO just for testing | ||||
|             if (courseName == "TEST_A" || courseName == "TEST_B") { | ||||
|                 val currentTime = System.currentTimeMillis() / 1000 | ||||
|             var timetable = TimetableWeek() | ||||
|             var weekNumberYear = 0 | ||||
|                 val timetableLink = "https://mosad.xyz" | ||||
|                 val weekNumberYear = 0 | ||||
|                 val year = 0 | ||||
|                 val instr = CacheController::class.java.getResourceAsStream("/html/Timetable_normal-week.html") | ||||
|  | ||||
|             // check if the timetable already exists and is up to date | ||||
|             when (timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null)) { | ||||
|                 // there is no such course yet, create one | ||||
|                 null -> { | ||||
|                     val courseLink = courseList.courses.stream().filter { x -> x.courseName == courseName }.findFirst().orElse(null).courseLink | ||||
|                     val timetableLink = courseLink.replace("week=0","week=$weekIndex") | ||||
|                 val timetableParser = | ||||
|                     TimetableParser(htmlDoc = Jsoup.parse(instr, "UTF-8", "https://www.hs-offenburg.de/")) | ||||
|                 val timetableTest = timetableParser.parseTimeTable() | ||||
|  | ||||
|                     val jobTimetable = async { | ||||
|                         timetable = TimetableParser().getTimeTable(timetableLink) | ||||
|                         weekNumberYear = TimetableParser().getWeekNumberYear(timetableLink) | ||||
|                 return TimetableCourseWeek( | ||||
|                     TimetableCourseMeta( | ||||
|                         currentTime, | ||||
|                         courseName, | ||||
|                         weekIndex, | ||||
|                         weekNumberYear, | ||||
|                         year, | ||||
|                         timetableLink | ||||
|                     ), timetableTest ?: TimetableWeek() | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|                     jobTimetable.await() | ||||
|                     timetableList.add(TimetableCourseWeek(TimetableCourseMeta(currentTime, courseName, weekIndex, weekNumberYear, timetableLink), timetable)) | ||||
|             val key = "$courseName-$weekIndex" | ||||
|             return if (timetableList.contains(key)) { | ||||
|                 timetableList[key]!! | ||||
|             } else { | ||||
|                 val timetableLink = courseList.courses[courseName] | ||||
|                     ?.courseLink | ||||
|                     ?.replace("week=0", "week=$weekIndex") ?: "" | ||||
|                 val currentTime = System.currentTimeMillis() / 1000 | ||||
|  | ||||
|                     logger.info("added new timetable for $courseName, week $weekIndex") | ||||
|                 } | ||||
|             } | ||||
|                 val timetableParser = TimetableParser(timetableLink) | ||||
|                 val calendarWeek = timetableParser.parseCalendarWeek() | ||||
|                 val timetable = timetableParser.parseTimeTable() | ||||
|  | ||||
|             return@runBlocking timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null) | ||||
|                 TimetableCourseWeek( | ||||
|                     TimetableCourseMeta( | ||||
|                         currentTime, | ||||
|                         courseName, | ||||
|                         weekIndex, | ||||
|                         calendarWeek?.week ?: 0, | ||||
|                         calendarWeek?.year ?: 0, | ||||
|                         timetableLink | ||||
|                     ), timetable ?: TimetableWeek() | ||||
|                 ).also { if (timetable != null) timetableList[key] = it } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @ -130,19 +154,23 @@ class CacheController { | ||||
|  | ||||
|         /** | ||||
|          * this function updates the courseList | ||||
|          * during the update process the old data will be returned for a API request | ||||
|          * during the update process the old data will be returned for an API request | ||||
|          */ | ||||
|         private fun asyncUpdateCourseList() = GlobalScope.launch { | ||||
|             CourseListParser().getCourseLinks(StartupController.courseListURL)?.let { | ||||
|                 courseList = CoursesList(CoursesMeta(System.currentTimeMillis() / 1000, it.size), it) | ||||
|                 courseList = CoursesList(CoursesMeta(System.currentTimeMillis() / 1000, it.size), it.toSortedMap()) | ||||
|             } | ||||
|  | ||||
|             // 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 | ||||
|          * during the update process the old data will be returned for an API request | ||||
|          */ | ||||
|         private fun asyncUpdateMensa() = GlobalScope.launch { | ||||
|             val mensaCurrentWeek = MensaParser().getMensaMenu(StartupController.mensaMenuURL) | ||||
| @ -158,7 +186,7 @@ class CacheController { | ||||
|  | ||||
|         /** | ||||
|          * this function updates all existing timetables | ||||
|          * during the update process the old data will be returned for a API request | ||||
|          * during the update process the old data will be returned for an API request | ||||
|          * a FixedThreadPool is used to make parallel requests for faster updates | ||||
|          */ | ||||
|         private fun asyncUpdateTimetables() = GlobalScope.launch { | ||||
| @ -170,10 +198,13 @@ class CacheController { | ||||
|             try { | ||||
|                 timetableList.forEach { timetableCourse -> | ||||
|                     executor.execute { | ||||
|                         timetableCourse.timetable = TimetableParser().getTimeTable(timetableCourse.meta.link) | ||||
|                         timetableCourse.meta.updateTime = System.currentTimeMillis() / 1000 | ||||
|                         val timetableParser = TimetableParser(timetableCourse.value.meta.link) | ||||
|                         timetableCourse.value.timetable = timetableParser.parseTimeTable() ?: return@execute | ||||
|                         timetableCourse.value.meta.weekNumberYear = | ||||
|                             timetableParser.parseCalendarWeek()?.week ?: return@execute | ||||
|                         timetableCourse.value.meta.updateTime = System.currentTimeMillis() / 1000 | ||||
|  | ||||
|                         saveTimetableToCache(timetableCourse) // save the updated timetable to the cache directory | ||||
|                         saveTimetableToCache(timetableCourse.value) // save the updated timetable to the cache directory | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
| @ -190,13 +221,17 @@ class CacheController { | ||||
|          * @param timetable a timetable of the type [TimetableCourseWeek] | ||||
|          */ | ||||
|         private fun saveTimetableToCache(timetable: TimetableCourseWeek) { | ||||
|             println(timetable.timetable.toString()) | ||||
|  | ||||
|             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 | ||||
| @ -217,30 +252,39 @@ class CacheController { | ||||
|          * 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! | ||||
|          */ | ||||
|         @OptIn(ExperimentalTime::class) | ||||
|         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 | ||||
|  | ||||
|             val duration24h = Duration.hours(24).inWholeMilliseconds | ||||
|             val duration3h = Duration.hours(3).inWholeMilliseconds | ||||
|             val duration1h = Duration.hours(1).inWholeMilliseconds | ||||
|             val duration1m = Duration.minutes(1).inWholeMilliseconds | ||||
|  | ||||
|             // 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) | ||||
|  | ||||
|             // update courseList every 24 hours (time in ms) | ||||
|             Timer().scheduleAtFixedRate(initDelay24h, 86400000) { | ||||
|             Timer().scheduleAtFixedRate(initDelay24h, duration24h) { | ||||
|                 asyncUpdateCourseList() | ||||
|             } | ||||
|  | ||||
|             // update all already existing timetables every 3 hours (time in ms) | ||||
|             Timer().scheduleAtFixedRate(initDelay3h, 10800000) { | ||||
|             Timer().scheduleAtFixedRate(initDelay3h, duration3h) { | ||||
|                 asyncUpdateTimetables() | ||||
|             } | ||||
|  | ||||
|             // update courses every hour (time in ms) | ||||
|             Timer().scheduleAtFixedRate(initDelay1h, 3600000) { | ||||
|             Timer().scheduleAtFixedRate(initDelay1h, duration1h) { | ||||
|                 asyncUpdateMensa() | ||||
|             } | ||||
|  | ||||
|             // post to status.mosad.xyz every hour, if an API key is present | ||||
|             if (StartupController.cachetAPIKey != "0") { | ||||
|                 Timer().scheduleAtFixedRate(initDelay1h, 3600000) { | ||||
|                 Timer().scheduleAtFixedRate(initDelay1h, duration1h) { | ||||
|                     CachetAPIController.postTotalRequests() | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @ -22,7 +22,7 @@ | ||||
|  | ||||
| package org.mosad.thecitadelofricks.controller | ||||
|  | ||||
| import org.mosad.thecitadelofricks.controller.StatusController.Companion.getTotalRequests | ||||
| import org.mosad.thecitadelofricks.controller.StatusController.Companion.totalRequests | ||||
| import org.slf4j.Logger | ||||
| import org.slf4j.LoggerFactory | ||||
| import java.io.BufferedReader | ||||
| @ -42,8 +42,8 @@ class CachetAPIController { | ||||
|          fun postTotalRequests() { | ||||
|              try { | ||||
|                  val url = URL("${StartupController.cachetBaseURL}/api/v1/metrics/1/points") | ||||
|                  val jsonInputString = "{\"value\": ${getTotalRequests() -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}" | ||||
|                  oldTotalRequests = getTotalRequests() | ||||
|                  val jsonInputString = "{\"value\": ${totalRequests -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}" | ||||
|                  oldTotalRequests = totalRequests | ||||
|  | ||||
|                  val con = url.openConnection() as HttpURLConnection | ||||
|                  con.requestMethod = "POST" | ||||
|  | ||||
| @ -44,7 +44,7 @@ class StartupController { | ||||
|         var cachetAPIKey = "0" | ||||
|         var cachetBaseURL = "https://status.mosad.xyz" | ||||
|         var courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/" | ||||
|         var mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||
|         var mensaMenuURL = "https://www.swfr.de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||
|         var mensaName = "Offenburg" | ||||
|     } | ||||
|  | ||||
| @ -92,7 +92,7 @@ class StartupController { | ||||
|         mensaMenuURL = try { | ||||
|             properties.getProperty("mensaMenuURL") | ||||
|         } catch (ex: Exception) { | ||||
|             "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||
|             "https://www.swfr.de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||
|         } | ||||
|  | ||||
|         mensaName = try { | ||||
| @ -113,7 +113,7 @@ class StartupController { | ||||
|  | ||||
|         properties.setProperty("cachetAPIKey", "0") | ||||
|         properties.setProperty("cachetBaseURL", "https://status.mosad.xyz") | ||||
|         properties.setProperty("mensaMenuURL", "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/") | ||||
|         properties.setProperty("mensaMenuURL", "https://www.swfr.de/essen-trinken/speiseplaene/mensa-offenburg/") | ||||
|         properties.setProperty("mensaName", "Offenburg") | ||||
|  | ||||
|         val outputStream = FileOutputStream(fileConfig) | ||||
| @ -134,7 +134,9 @@ class StartupController { | ||||
|  | ||||
|                 try { | ||||
|                     val timetableObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject | ||||
|                     CacheController.timetableList.add(Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass)) | ||||
|                     val timetable = Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass) | ||||
|                     CacheController.timetableList["${timetable.meta.courseName}-${timetable.meta.weekIndex}"] = | ||||
|                         timetable | ||||
|                 } catch (ex: Exception) { | ||||
|                     logger.error("error while reading cache", ex) | ||||
|                 } finally { | ||||
|  | ||||
| @ -26,24 +26,27 @@ import org.mosad.thecitadelofricks.APIController.Companion.apiVersion | ||||
| import org.mosad.thecitadelofricks.APIController.Companion.softwareVersion | ||||
| import org.mosad.thecitadelofricks.APIController.Companion.startTime | ||||
| import org.mosad.thecitadelofricks.Status | ||||
| import org.mosad.thecitadelofricks.TimetableCounter | ||||
| import org.slf4j.Logger | ||||
| import org.slf4j.LoggerFactory | ||||
| import java.net.HttpURLConnection | ||||
| import java.net.URL | ||||
| import java.time.LocalDateTime | ||||
| import java.util.* | ||||
| import kotlin.collections.ArrayList | ||||
| import kotlin.collections.HashMap | ||||
|  | ||||
| class StatusController { | ||||
|  | ||||
|     companion object { | ||||
|         private val logger: Logger = LoggerFactory.getLogger(StatusController::class.java) | ||||
|  | ||||
|         private var totalRequests = 0 | ||||
|         private var mensaMenuRequests = 0 | ||||
|         private var courseListRequests = 0 | ||||
|         private var timetableRequests = ArrayList<TimetableCounter>() | ||||
|         var totalRequests = 0 | ||||
|             private set | ||||
|         var mensaMenuRequests = 0 | ||||
|             private set | ||||
|         var courseListRequests = 0 | ||||
|             private set | ||||
|         var timetableRequests = HashMap<String, Int>() | ||||
|             private set | ||||
|  | ||||
|         /** | ||||
|          * if a mensamenu/courseList/timetable is requested update the specific and total request count | ||||
| @ -59,33 +62,10 @@ class StatusController { | ||||
|         } | ||||
|  | ||||
|         fun updateTimetableRequests(courseName: String) { | ||||
|             timetableRequests.stream().filter { it.courseName == courseName }.findFirst().ifPresentOrElse({ | ||||
|                 it.requests++ | ||||
|             }, { | ||||
|                 timetableRequests.add(TimetableCounter(courseName, 1)) | ||||
|             }) | ||||
|             timetableRequests[courseName] = (timetableRequests[courseName] ?: 0) + 1 | ||||
|             totalRequests++ | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * getters and setters | ||||
|          */ | ||||
|         fun getTotalRequests(): Int { | ||||
|             return totalRequests | ||||
|         } | ||||
|  | ||||
|         fun getMensaMenuRequests(): Int { | ||||
|             return mensaMenuRequests | ||||
|         } | ||||
|  | ||||
|         fun getCourseListRequests(): Int { | ||||
|             return courseListRequests | ||||
|         } | ||||
|  | ||||
|         fun getTimetableRequests(): ArrayList<TimetableCounter> { | ||||
|             return timetableRequests | ||||
|         } | ||||
|  | ||||
|         fun getStatus(): Status { | ||||
|             val currentTime = System.currentTimeMillis() / 1000 | ||||
|             val minutes = (currentTime - startTime) % 3600 / 60 | ||||
| @ -117,10 +97,10 @@ class StatusController { | ||||
|                 "$days days, $hours:$minutes", | ||||
|                 apiVersion, | ||||
|                 softwareVersion, | ||||
|                 getTotalRequests(), | ||||
|                 getMensaMenuRequests(), | ||||
|                 getCourseListRequests(), | ||||
|                 getTimetableRequests(), | ||||
|                 totalRequests, | ||||
|                 mensaMenuRequests, | ||||
|                 courseListRequests, | ||||
|                 timetableRequests, | ||||
|                 CacheController.timetableList.size, | ||||
|                 Date(CacheController.courseList.meta.updateTime * 1000), | ||||
|                 Date(CacheController.mensaMenu.meta.updateTime * 1000), | ||||
|  | ||||
| @ -29,26 +29,25 @@ import java.net.SocketTimeoutException | ||||
|  | ||||
| class CourseListParser { | ||||
|  | ||||
|     var logger: org.slf4j.Logger = LoggerFactory.getLogger(MensaParser::class.java) | ||||
|     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): ArrayList<Course>? { | ||||
|         val courseLinkList = ArrayList<Course>() | ||||
|     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.add( | ||||
|                     Course( | ||||
|                 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 | ||||
|  | ||||
| @ -22,34 +22,50 @@ | ||||
|  | ||||
| package org.mosad.thecitadelofricks.hsoparser | ||||
|  | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import kotlinx.coroutines.sync.Semaphore | ||||
| import org.jsoup.Jsoup | ||||
| import org.jsoup.nodes.Document | ||||
| import org.mosad.thecitadelofricks.CalendarWeek | ||||
| import org.mosad.thecitadelofricks.Lesson | ||||
| import org.mosad.thecitadelofricks.TimetableWeek | ||||
| import org.slf4j.LoggerFactory | ||||
|  | ||||
| class TimetableParser { | ||||
| /** | ||||
|  * @param timetableURL the URL of the timetable you want to get | ||||
|  * @param htmlDoc the html document to use (the timetableURL will be ignored if this value is present) | ||||
|  */ | ||||
| class TimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) { | ||||
|     private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableParser::class.java) | ||||
|     private val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") | ||||
|  | ||||
|     /** | ||||
|      * get the timetable from the given url | ||||
|      * the timetable is organised per row not per column; | ||||
|      * Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on | ||||
|      * @param timetableURL the URL of the timetable you want to get | ||||
|      */ | ||||
|     fun getTimeTable(timetableURL: String): TimetableWeek { | ||||
|         return try { | ||||
|             parseTimeTable(Jsoup.connect(timetableURL).get()) | ||||
|     companion object { | ||||
|         val semaphore = Semaphore(3, 0) | ||||
|     } | ||||
|  | ||||
|     private val htmlDoc: Document? = htmlDoc ?: timetableURL?.let { | ||||
|         runBlocking { | ||||
|             try { | ||||
|                 // Only allow sending a limited amount of requests at the same time | ||||
|                 semaphore.acquire() | ||||
|                 Jsoup.connect(timetableURL).get() | ||||
|             } catch (gex: Exception) { | ||||
|                 logger.error("general TimetableParser error", gex) | ||||
|             TimetableWeek() | ||||
|                 null | ||||
|             } finally { | ||||
|                 semaphore.release() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun parseTimeTable(htmlDoc: Document): TimetableWeek { | ||||
|     /** | ||||
|      * parse the timetable from the previously given url | ||||
|      * the timetable is organised per row not per column; | ||||
|      * Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on | ||||
|      */ | ||||
|     fun parseTimeTable(): TimetableWeek? = htmlDoc?.let { | ||||
|         val timetableWeek = TimetableWeek() | ||||
|         val rows = htmlDoc.select("table.timetable").select("tr[scope=\"row\"]") | ||||
|         val rows = it.select("table.timetable").select("tr[scope=\"row\"]") | ||||
|  | ||||
|         var sDay = -1 | ||||
|         var sRow = -1 | ||||
| @ -104,8 +120,7 @@ class TimetableParser { | ||||
|  | ||||
|                 lessonIndexDay++ | ||||
|  | ||||
|                 if (element.hasClass("lastcol")) | ||||
|                 { | ||||
|                 if (element.hasClass("lastcol")) { | ||||
|                     day++ | ||||
|                     lessonIndexDay = 0 | ||||
|                 } | ||||
| @ -117,21 +132,13 @@ class TimetableParser { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * get the week number of the year for the timetable | ||||
|      * @param timetableURL the URL of the timetable you want to get | ||||
|      * parse the calendar week and the associated year for the timetable | ||||
|      */ | ||||
|     fun getWeekNumberYear(timetableURL: String): Int { | ||||
|         return try { | ||||
|             parseWeekNumberYear(Jsoup.connect(timetableURL).get()) | ||||
|         } catch (gex: Exception) { | ||||
|             logger.error("general TimetableParser error", gex) | ||||
|             0 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun parseWeekNumberYear(htmlDoc: Document): Int { | ||||
|         return htmlDoc.select("h1.timetable-caption").text().substringAfter("- ") | ||||
|             .substringBefore(".").replace(" ", "").toInt() | ||||
|     fun parseCalendarWeek(): CalendarWeek? = htmlDoc?.let { | ||||
|         val dateStr = it.select("h1.timetable-caption").text().substringAfter("- ") | ||||
|         val week = dateStr.substringBefore(".").replace(" ", "").toInt() | ||||
|         val year = dateStr.substringAfter("Woche ").replace(" ", "").toInt() | ||||
|         CalendarWeek(week, year) | ||||
|     } | ||||
|  | ||||
|     @Suppress("unused") | ||||
| @ -181,4 +188,5 @@ class TimetableParser { | ||||
|  | ||||
|         println(" \n") | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -11,5 +11,5 @@ logging.level.org.springframework.web=INFO | ||||
| # ---------------------------------------- | ||||
|  | ||||
| # EMBEDDED SERVER CONFIGURATION (ServerProperties) | ||||
| server.address=127.0.0.1 | ||||
| server.address=0.0.0.0 | ||||
| server.port=8080 | ||||
							
								
								
									
										1726
									
								
								src/main/resources/html/Timetable_normal-week.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1726
									
								
								src/main/resources/html/Timetable_normal-week.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -28,7 +28,7 @@ import org.junit.jupiter.api.Test | ||||
| import java.io.File | ||||
|  | ||||
| internal class MensaParserTest { | ||||
|     private val mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||
|     private val mensaMenuURL = "https://www.swfr.de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||
|  | ||||
|     @Test | ||||
|     fun parseMensaMenuNormalWeek() { | ||||
|  | ||||
| @ -25,6 +25,7 @@ package org.mosad.thecitadelofricks.hsoparser | ||||
| import org.jsoup.Jsoup | ||||
| import org.junit.jupiter.api.Assertions | ||||
| import org.junit.jupiter.api.Test | ||||
| import org.mosad.thecitadelofricks.CalendarWeek | ||||
| import java.io.File | ||||
|  | ||||
| class TimetableParserTest { | ||||
| @ -33,7 +34,7 @@ class TimetableParserTest { | ||||
|     fun parseTimetableNormalWeek() { | ||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) | ||||
|         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||
|         val actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() | ||||
|         val actualTimetable = TimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() | ||||
|         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_normal-week.txt").readText().trim() | ||||
|  | ||||
|         Assertions.assertEquals(expectedTimetable, actualTimetable) | ||||
| @ -43,18 +44,18 @@ class TimetableParserTest { | ||||
|     fun parseTimetableEmptyWeek() { | ||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_empty-week.html").path) | ||||
|         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||
|         val actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() | ||||
|         val actualTimetable = TimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() | ||||
|         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_empty-week.txt").readText().trim() | ||||
|  | ||||
|         Assertions.assertEquals(expectedTimetable, actualTimetable) | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun parseWeekNumberYear() { | ||||
|     fun parseCalendarWeek() { | ||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) | ||||
|         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||
|         val actualWeekNumberYear = TimetableParser().parseWeekNumberYear(htmlDoc) | ||||
|         val actualCalendarWeek = TimetableParser(htmlDoc = htmlDoc).parseCalendarWeek() | ||||
|  | ||||
|         Assertions.assertEquals(42, actualWeekNumberYear) | ||||
|         Assertions.assertEquals(CalendarWeek(42, 2019), actualCalendarWeek) | ||||
|     } | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| <div id="speiseplan-tabs"> | ||||
|     <div id="tab-menu-container" class="row"> | ||||
|         <div class="col-md-1 col-xs-6"> | ||||
|             <a class="prev-week" title="eine Woche zurück" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=ad074b11cc01a680e4c0d0675dd1f91c"><img src="/fileadmin/templates/images/keil-grau-links.png" width="16" height="16" alt="" /><span class="d-inline d-sm-none">vorherige Woche</span></a> | ||||
|             <a class="prev-week" title="eine Woche zurück" href="/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=ad074b11cc01a680e4c0d0675dd1f91c"><img src="/fileadmin/templates/images/keil-grau-links.png" width="16" height="16" alt="" /><span class="d-inline d-sm-none">vorherige Woche</span></a> | ||||
|         </div> | ||||
|         <div class="col-md-10"> | ||||
|             <ul class="nav classic-tabs tabs-primary" role="tablist"> | ||||
| @ -13,7 +13,7 @@ | ||||
|                 <li class="col-md-2"><a href="#tab-sat" class="nav-link" data-toggle="tab" role="tab">Sa 14.03.</a></li> | ||||
|             </ul> | ||||
|         </div> | ||||
|         <div class="col-md-1 col-xs-6 pull-right"><a class="next-week text-right" title="eine Woche weiter" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=2&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=3c3048ebdd7f42a104bd780a0a6a0fd1"><span class="d-inline d-sm-none">nächste Woche</span><img src="/fileadmin/templates/images/keil-grau-rechts.png" width="16" height="16" alt="" /></a></div> | ||||
|         <div class="col-md-1 col-xs-6 pull-right"><a class="next-week text-right" title="eine Woche weiter" href="/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=2&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=3c3048ebdd7f42a104bd780a0a6a0fd1"><span class="d-inline d-sm-none">nächste Woche</span><img src="/fileadmin/templates/images/keil-grau-rechts.png" width="16" height="16" alt="" /></a></div> | ||||
|     </div> | ||||
|     <div class="row"> | ||||
|         <div class="col-sm-4"> | ||||
| @ -34,12 +34,12 @@ | ||||
|         </div> | ||||
|         <div class="col-sm-4 pt-3"> | ||||
|             <div class="hide-with-allergenes"> | ||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=4de091579c564e42f79473da2701f179"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> | ||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=84edf4562bdc95f103b70ef99e4d7af6"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> | ||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" href="/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=4de091579c564e42f79473da2701f179"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> | ||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" href="/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=84edf4562bdc95f103b70ef99e4d7af6"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> | ||||
|             </div> | ||||
|             <div class="show-with-allergenes"> | ||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=51f0f1e0083f9ef48038920dfb676eb6"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> | ||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=c3a9d1eafce78d8f8d281c6060be989c"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> | ||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" href="/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=51f0f1e0083f9ef48038920dfb676eb6"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> | ||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" href="/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=c3a9d1eafce78d8f8d281c6060be989c"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-sm-4 pt-3"> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user