diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt b/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt index beb97dd..cba4a89 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt @@ -1,6 +1,6 @@ package de.twomartens.timetable -import de.twomartens.timetable.service.ScheduledTaskService +import de.twomartens.timetable.bahnApi.service.ScheduledTaskService import mu.KotlinLogging import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.event.ApplicationReadyEvent diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/configuration/ThreadPoolTaskExecutorConfig.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/configuration/ThreadPoolTaskExecutorConfig.kt similarity index 90% rename from module-server/src/main/kotlin/de/twomartens/timetable/configuration/ThreadPoolTaskExecutorConfig.kt rename to module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/configuration/ThreadPoolTaskExecutorConfig.kt index 8481538..b6e441d 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/configuration/ThreadPoolTaskExecutorConfig.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/configuration/ThreadPoolTaskExecutorConfig.kt @@ -1,4 +1,4 @@ -package de.twomartens.timetable.configuration +package de.twomartens.timetable.bahnApi.configuration import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -8,6 +8,7 @@ private const val CORE_POOL_SIZE = 1 @Configuration open class ThreadPoolTaskExecutorConfig { + @Bean open fun threadPoolTaskExecutor(): ThreadPoolTaskExecutor { val executor = ThreadPoolTaskExecutor() diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/configuration/ThreadPoolTaskSchedulerConfig.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/configuration/ThreadPoolTaskSchedulerConfig.kt similarity index 91% rename from module-server/src/main/kotlin/de/twomartens/timetable/configuration/ThreadPoolTaskSchedulerConfig.kt rename to module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/configuration/ThreadPoolTaskSchedulerConfig.kt index c18d3ce..6d5f27f 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/configuration/ThreadPoolTaskSchedulerConfig.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/configuration/ThreadPoolTaskSchedulerConfig.kt @@ -1,11 +1,10 @@ -package de.twomartens.timetable.configuration +package de.twomartens.timetable.bahnApi.configuration import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler private const val POOL_SIZE = 1 - private const val THREAD_NAME_PREFIX = "ThreadPoolTaskScheduler" @Configuration diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/jaxb/LocalDateTimeAdapter.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/jaxb/LocalDateTimeAdapter.kt index fe7408c..9e9360e 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/jaxb/LocalDateTimeAdapter.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/jaxb/LocalDateTimeAdapter.kt @@ -3,17 +3,18 @@ package de.twomartens.timetable.bahnApi.jaxb import jakarta.xml.bind.annotation.adapters.XmlAdapter import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException import java.util.* class LocalDateTimeAdapter : XmlAdapter() { - @Throws(Exception::class) + @Throws(DateTimeParseException::class) override fun unmarshal(xmlValue: String?): LocalDateTime { return LocalDateTime.parse(xmlValue, dateFormatter.get()) } - @Throws(Exception::class) + @Throws(DateTimeParseException::class) override fun marshal(kotlinValue: LocalDateTime): String? { return dateFormatter.get().format(kotlinValue) } diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/mapper/BahnTimetableMapper.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/mapper/BahnTimetableMapper.kt index e2ceb51..490c5f1 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/mapper/BahnTimetableMapper.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/mapper/BahnTimetableMapper.kt @@ -1,6 +1,7 @@ package de.twomartens.timetable.bahnApi.mapper import de.twomartens.timetable.bahnApi.model.db.BahnTimetable +import de.twomartens.timetable.model.base.NonEmptyString import org.mapstruct.* import java.time.LocalDateTime @@ -16,9 +17,9 @@ interface BahnTimetableMapper { fun mapToDB(dto: de.twomartens.timetable.bahnApi.model.dto.BahnTimetable, dateTime: LocalDateTime): BahnTimetable { return BahnTimetable( - dto.eva!!, + dto.eva, dateTime, - dto.station, + NonEmptyString(dto.station), dto.stops ) } diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/Eva.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/Eva.kt new file mode 100644 index 0000000..19a8ee6 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/Eva.kt @@ -0,0 +1,24 @@ +package de.twomartens.timetable.bahnApi.model + +import de.twomartens.timetable.model.db.StationId + +@JvmInline +value class Eva(val value: Int) { + init { + if (isKnown()) { + require(value in 8000000..8999999) { + "Eva number must start with an 8 and have 7 digits" + } + } + } + + private fun isKnown() = value != -1 + + companion object { + val UNKNOWN = Eva(-1) + + fun of(stationId: StationId): Eva { + return Eva(stationId.stationIdWithinCountry.name.toInt()) + } + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/FetchDates.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/FetchDates.kt new file mode 100644 index 0000000..bdf609a --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/FetchDates.kt @@ -0,0 +1,5 @@ +package de.twomartens.timetable.bahnApi.model + +import java.time.LocalDate + +data class FetchDates(val previousDay: LocalDate, val fetchDate: LocalDate, val nextDate: LocalDate) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/TaskFactory.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/TaskFactory.kt new file mode 100644 index 0000000..54c7055 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/TaskFactory.kt @@ -0,0 +1,58 @@ +package de.twomartens.timetable.bahnApi.model + +import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask +import de.twomartens.timetable.model.base.HourAtDay +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +private const val SECONDS_BETWEEN_TASKS = 2 + +private const val ZERO = 0 + +@Component +class TaskFactory { + + private val _tasksPerHour: MutableMap = mutableMapOf() + + fun initializeWithExistingTasks(existingTasks: List) { + existingTasks.forEach { countTask(it) } + } + + fun createTaskAndUpdateCounter(eva: Eva, hourAtDay: HourAtDay): ScheduledFetchTask { + val fetchDateTime = calculateNextAvailableFetchDateTime(hourAtDay) + incrementTaskCountForHourAtDay(hourAtDay) + return ScheduledFetchTask(eva, hourAtDay, fetchDateTime) + } + + private fun calculateNextAvailableFetchDateTime(hourAtDay: HourAtDay): LocalDateTime { + val secondInHourToExecute = calculateSecondInHourToCalculate(hourAtDay) + return calculateFetchDateTime(hourAtDay, secondInHourToExecute) + } + + private fun calculateSecondInHourToCalculate(hourAtDay: HourAtDay): Int { + val numberOfTasksInHour = getNumberOfTasksInHour(hourAtDay) + return numberOfTasksInHour * SECONDS_BETWEEN_TASKS + } + + private fun calculateFetchDateTime( + hourAtDay: HourAtDay, + secondInHourToExecute: Int + ): LocalDateTime { + return hourAtDay.dateTime.plusSeconds(secondInHourToExecute.toLong()) + } + + private fun countTask(scheduledFetchTask: ScheduledFetchTask) { + val scheduledTime = scheduledFetchTask.scheduledExecutionDateTime + val hourAtDay = HourAtDay.of(scheduledTime) + incrementTaskCountForHourAtDay(hourAtDay) + } + + private fun incrementTaskCountForHourAtDay(hourAtDay: HourAtDay) { + val numberOfTasksInHour = getNumberOfTasksInHour(hourAtDay) + _tasksPerHour[hourAtDay] = numberOfTasksInHour + 1 + } + + private fun getNumberOfTasksInHour(hourAtDay: HourAtDay) = + _tasksPerHour.computeIfAbsent(hourAtDay) { ZERO } + +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnStation.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnStation.kt index a1b48de..3d3bc65 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnStation.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnStation.kt @@ -1,7 +1,7 @@ package de.twomartens.timetable.bahnApi.model.db -import de.twomartens.timetable.model.Eva -import de.twomartens.timetable.model.NonEmptyString +import de.twomartens.timetable.bahnApi.model.Eva +import de.twomartens.timetable.model.base.NonEmptyString import org.bson.types.ObjectId import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.Id diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnTimetable.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnTimetable.kt index 41ce5b8..64fe59a 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnTimetable.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnTimetable.kt @@ -1,8 +1,8 @@ package de.twomartens.timetable.bahnApi.model.db +import de.twomartens.timetable.bahnApi.model.Eva import de.twomartens.timetable.bahnApi.model.dto.BahnStationStop -import de.twomartens.timetable.model.Eva -import de.twomartens.timetable.model.NonEmptyString +import de.twomartens.timetable.model.base.NonEmptyString import org.bson.types.ObjectId import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.Id diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/ScheduledFetchTask.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/ScheduledFetchTask.kt index ac7cd20..b553673 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/ScheduledFetchTask.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/ScheduledFetchTask.kt @@ -1,7 +1,7 @@ package de.twomartens.timetable.bahnApi.model.db -import de.twomartens.timetable.model.Eva -import de.twomartens.timetable.model.Hour +import de.twomartens.timetable.bahnApi.model.Eva +import de.twomartens.timetable.model.base.HourAtDay import org.bson.types.ObjectId import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.Id @@ -9,15 +9,13 @@ import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.mongodb.core.index.CompoundIndex import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant -import java.time.LocalDate import java.time.LocalDateTime @Document -@CompoundIndex(def = "{'eva': 1, 'fetchedDate': 1, 'fetchedTime': 1", unique = true) +@CompoundIndex(def = "{'eva': 1, 'fetchedDateTime': 1", unique = true) data class ScheduledFetchTask( var eva: Eva, - var fetchedDate: LocalDate, - var fetchedTime: Hour, + var fetchedDateTime: HourAtDay, var scheduledExecutionDateTime: LocalDateTime ) { @Id diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStation.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStation.kt index afc0027..3458b5d 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStation.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStation.kt @@ -1,20 +1,16 @@ package de.twomartens.timetable.bahnApi.model.dto -import de.twomartens.timetable.model.Eva +import de.twomartens.timetable.bahnApi.model.Eva import jakarta.xml.bind.annotation.XmlAccessType import jakarta.xml.bind.annotation.XmlAccessorType import jakarta.xml.bind.annotation.XmlAttribute @XmlAccessorType(XmlAccessType.FIELD) data class BahnStation( - @field:XmlAttribute - var name: String, - @field:XmlAttribute - var eva: Eva?, - @field:XmlAttribute - var ds100: String, - @field:XmlAttribute - var db: Boolean + @field:XmlAttribute var name: String, + @field:XmlAttribute var eva: Eva, + @field:XmlAttribute var ds100: String, + @field:XmlAttribute var db: Boolean ) { - constructor(): this("", null, "", true) + constructor() : this("", Eva.UNKNOWN, "", true) } \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStationStop.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStationStop.kt index d36337b..c681254 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStationStop.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStationStop.kt @@ -1,6 +1,6 @@ package de.twomartens.timetable.bahnApi.model.dto -import de.twomartens.timetable.model.Eva +import de.twomartens.timetable.bahnApi.model.Eva import jakarta.xml.bind.annotation.XmlAccessType import jakarta.xml.bind.annotation.XmlAccessorType import jakarta.xml.bind.annotation.XmlAttribute @@ -8,11 +8,11 @@ import jakarta.xml.bind.annotation.XmlElement @XmlAccessorType(XmlAccessType.FIELD) data class BahnStationStop( - @field:XmlAttribute var eva: Eva?, - @field:XmlAttribute var id: String, - @field:XmlElement(name = "tl") var tripLabel: BahnTripLabel?, - @field:XmlElement(name = "ar") var arrival: BahnStopEvent?, - @field:XmlElement(name = "dp") var departure: BahnStopEvent? + @field:XmlAttribute var eva: Eva, + @field:XmlAttribute var id: String, + @field:XmlElement(name = "tl") var tripLabel: BahnTripLabel, + @field:XmlElement(name = "ar") var arrival: BahnStopEvent?, + @field:XmlElement(name = "dp") var departure: BahnStopEvent? ) { - constructor(): this(null, "", null, null, null) + constructor() : this(Eva.UNKNOWN, "", BahnTripLabel(), null, null) } \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStations.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStations.kt index 6a9ea4b..b9a71e8 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStations.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStations.kt @@ -10,5 +10,5 @@ import jakarta.xml.bind.annotation.XmlRootElement data class BahnStations( @field:XmlElement(name = "station") var stations: List ) { - constructor(): this(mutableListOf()) + constructor() : this(listOf()) } diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTimetable.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTimetable.kt index 45cc8c8..0792eb9 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTimetable.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTimetable.kt @@ -1,14 +1,14 @@ package de.twomartens.timetable.bahnApi.model.dto -import de.twomartens.timetable.model.Eva +import de.twomartens.timetable.bahnApi.model.Eva import jakarta.xml.bind.annotation.* @XmlRootElement(name = "timetable") @XmlAccessorType(XmlAccessType.FIELD) data class BahnTimetable( - @field:XmlAttribute var eva: Eva?, - @field:XmlAttribute var station: String, - @field:XmlElement(name = "s") var stops: List + @field:XmlAttribute var eva: Eva, + @field:XmlAttribute var station: String, + @field:XmlElement(name = "s") var stops: List ) { - constructor(): this(null, "", mutableListOf()) + constructor() : this(Eva.UNKNOWN, "", listOf()) } \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripLabel.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripLabel.kt index 47b8418..4ee5197 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripLabel.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripLabel.kt @@ -11,8 +11,7 @@ data class BahnTripLabel( @field:XmlAttribute(name = "f") var filterFlags: String, @field:XmlAttribute(name = "n") var trainNumber: String, @field:XmlAttribute(name = "o") var owner: String, - @field:XmlAttribute(name = "t") var tripType: BahnTripType? + @field:XmlAttribute(name = "t") var tripType: BahnTripType ) { - constructor(): this("", "", "", "", - null) + constructor() : this("", "", "", "", BahnTripType.p) } \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt index 3459b15..cca994e 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt @@ -1,18 +1,17 @@ package de.twomartens.timetable.bahnApi.service +import de.twomartens.timetable.bahnApi.model.Eva import de.twomartens.timetable.bahnApi.model.dto.BahnStation import de.twomartens.timetable.bahnApi.model.dto.BahnStations import de.twomartens.timetable.bahnApi.model.dto.BahnTimetable import de.twomartens.timetable.bahnApi.property.BahnApiProperties -import de.twomartens.timetable.model.Eva -import de.twomartens.timetable.model.Hour +import de.twomartens.timetable.model.base.HourAtDay import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.MediaType import org.springframework.stereotype.Service import org.springframework.web.client.RestTemplate -import java.time.LocalDate import java.time.LocalTime import java.time.format.DateTimeFormatter @@ -33,14 +32,14 @@ class BahnApiService( return body?.stations ?: listOf() } - fun fetchTimetable(eva: Eva, date: LocalDate, hour: Hour): BahnTimetable { + fun fetchTimetable(eva: Eva, hourAtDay: HourAtDay): BahnTimetable { val requestEntity = buildRequestEntity() val dateFormatter = DateTimeFormatter.ofPattern("yyMMdd") val timeFormatter = DateTimeFormatter.ofPattern("HH") - val time = LocalTime.of(hour.value, 0) + val time = LocalTime.of(hourAtDay.hour.value, 0) val response = restTemplate.exchange( "https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1/plan/" + - "${eva}/${date.format(dateFormatter)}/${time.format(timeFormatter)}", + "${eva}/${hourAtDay.date.format(dateFormatter)}/${time.format(timeFormatter)}", HttpMethod.GET, requestEntity, BahnTimetable::class.java diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnDatabaseService.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnDatabaseService.kt index a61a96f..5ebc2dd 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnDatabaseService.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnDatabaseService.kt @@ -6,9 +6,9 @@ import de.twomartens.timetable.bahnApi.model.dto.BahnStation import de.twomartens.timetable.bahnApi.model.dto.BahnTimetable import de.twomartens.timetable.bahnApi.repository.BahnStationRepository import de.twomartens.timetable.bahnApi.repository.BahnTimetableRepository +import de.twomartens.timetable.model.base.HourAtDay import org.mapstruct.factory.Mappers import org.springframework.stereotype.Service -import java.time.LocalDateTime @Service class BahnDatabaseService( @@ -23,7 +23,7 @@ class BahnDatabaseService( .map { bahnStationMapper.mapToDB(it) }) } - fun storeTimetable(timetable: BahnTimetable, dateTime: LocalDateTime) { - bahnTimetableRepository.save(bahnTimetableMapper.mapToDB(timetable, dateTime)) + fun storeTimetable(timetable: BahnTimetable, hourAtDay: HourAtDay) { + bahnTimetableRepository.save(bahnTimetableMapper.mapToDB(timetable, hourAtDay.dateTime)) } } \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/ScheduledTaskService.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/ScheduledTaskService.kt new file mode 100644 index 0000000..9eb74ad --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/ScheduledTaskService.kt @@ -0,0 +1,77 @@ +package de.twomartens.timetable.bahnApi.service + +import de.twomartens.timetable.bahnApi.model.Eva +import de.twomartens.timetable.bahnApi.model.FetchDates +import de.twomartens.timetable.bahnApi.model.TaskFactory +import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask +import de.twomartens.timetable.bahnApi.repository.ScheduledFetchTaskRepository +import de.twomartens.timetable.model.base.Hour +import de.twomartens.timetable.model.base.HourAtDay +import de.twomartens.timetable.model.db.TswRoute +import mu.KotlinLogging +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class ScheduledTaskService( + private val scheduledFetchTaskRepository: ScheduledFetchTaskRepository, + private val taskFactory: TaskFactory, + private val taskScheduler: TaskScheduler +) { + fun initializeScheduledTasks() { + log.info { "Initialize persisted scheduled fetch tasks" } + val scheduledTasks = scheduledFetchTaskRepository.findAll() + taskFactory.initializeWithExistingTasks(scheduledTasks) + taskScheduler.scheduleFetchTasks(scheduledTasks) + } + + fun scheduleTimetableFetch(tswRoute: TswRoute, fetchedDate: LocalDate) { + log.info { + "Schedule timetable fetch: [route ${tswRoute.name}]" + } + val fetchDates = calculateDatesToFetch(fetchedDate) + val newTasks = buildScheduledTasks(tswRoute, fetchDates) + + storeTasksInDatabase(newTasks) + taskScheduler.scheduleFetchTasks(newTasks) + } + + private fun calculateDatesToFetch(fetchedDate: LocalDate): FetchDates { + val previousDay = fetchedDate.minusDays(1) + val nextDay = fetchedDate.plusDays(1) + return FetchDates(previousDay, fetchedDate, nextDay) + } + + private fun buildScheduledTasks( + tswRoute: TswRoute, + fetchDates: FetchDates + ): List { + val newTasks = mutableListOf() + tswRoute.stationIds.forEach { + val stationId = it + val eva = Eva.of(stationId) + var hourAtDay = HourAtDay.of(Hour.of(23), fetchDates.previousDay) + var newTask = taskFactory.createTaskAndUpdateCounter(eva, hourAtDay) + newTasks.add(newTask) + for (hour in 0..23) { + hourAtDay = HourAtDay.of(Hour.of(hour), fetchDates.fetchDate) + newTask = taskFactory.createTaskAndUpdateCounter(eva, hourAtDay) + newTasks.add(newTask) + } + for (hour in 0..3) { + hourAtDay = HourAtDay.of(Hour.of(hour), fetchDates.nextDate) + newTask = taskFactory.createTaskAndUpdateCounter(eva, hourAtDay) + newTasks.add(newTask) + } + } + return newTasks + } + + private fun storeTasksInDatabase(newTasks: List) { + scheduledFetchTaskRepository.saveAll(newTasks) + } + + companion object { + private val log = KotlinLogging.logger {} + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/TaskScheduler.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/TaskScheduler.kt new file mode 100644 index 0000000..66e10d0 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/TaskScheduler.kt @@ -0,0 +1,56 @@ +package de.twomartens.timetable.bahnApi.service + +import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask +import de.twomartens.timetable.bahnApi.model.dto.BahnTimetable +import de.twomartens.timetable.bahnApi.repository.ScheduledFetchTaskRepository +import de.twomartens.timetable.bahnApi.tasks.DeleteScheduledTask +import de.twomartens.timetable.bahnApi.tasks.FetchTimetableTask +import de.twomartens.timetable.bahnApi.tasks.StoreTimetableTask +import de.twomartens.timetable.model.base.HourAtDay +import de.twomartens.timetable.property.TimeProperties +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler +import org.springframework.stereotype.Service +import java.time.ZonedDateTime + +@Service +class TaskScheduler( + private val timeProperties: TimeProperties, + private val threadPoolTaskScheduler: ThreadPoolTaskScheduler, + private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, + private val bahnApiService: BahnApiService, + private val bahnDatabaseService: BahnDatabaseService, + private val scheduledFetchTaskRepository: ScheduledFetchTaskRepository +) { + fun scheduleFetchTasks(scheduledTasks: List) { + scheduledTasks.forEach { + val zonedExecutionTime = it.scheduledExecutionDateTime + .atZone(timeProperties.defaultTimeZone) + scheduleDeleteTask(it, zonedExecutionTime) + scheduleFetchTimetableTask(it, zonedExecutionTime) + } + } + + fun scheduleStoreTask(timetable: BahnTimetable, hourAtDay: HourAtDay) { + val storeTask = StoreTimetableTask(timetable, hourAtDay, bahnDatabaseService) + threadPoolTaskExecutor.execute(storeTask) + } + + private fun scheduleDeleteTask( + scheduledFetchTask: ScheduledFetchTask, + zonedExecutionTime: ZonedDateTime + ) { + val deleteTask = DeleteScheduledTask(scheduledFetchTaskRepository, scheduledFetchTask) + threadPoolTaskScheduler.schedule(deleteTask, zonedExecutionTime.toInstant()) + } + + private fun scheduleFetchTimetableTask( + scheduledFetchTask: ScheduledFetchTask, + zonedExecutionTime: ZonedDateTime + ) { + val timetableTask = FetchTimetableTask(scheduledFetchTask.eva, + scheduledFetchTask.fetchedDateTime, + bahnApiService, this) + threadPoolTaskScheduler.schedule(timetableTask, zonedExecutionTime.toInstant()) + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/DeleteScheduledTask.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/DeleteScheduledTask.kt index 9c91985..8189073 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/DeleteScheduledTask.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/DeleteScheduledTask.kt @@ -14,8 +14,8 @@ open class DeleteScheduledTask( log.info { "Delete scheduled fetch task after the fetch occurred: " + "[eva: ${scheduledFetchTask.eva}], " + - "[date: ${scheduledFetchTask.fetchedDate}], " + - "[time: ${scheduledFetchTask.fetchedTime}]" + "[date: ${scheduledFetchTask.fetchedDateTime.date}], " + + "[time: ${scheduledFetchTask.fetchedDateTime.hour}]" } scheduledFetchTaskRepository.delete(scheduledFetchTask) } diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/FetchTimetableTask.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/FetchTimetableTask.kt index 9351d48..2e8073b 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/FetchTimetableTask.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/FetchTimetableTask.kt @@ -1,30 +1,24 @@ package de.twomartens.timetable.bahnApi.tasks +import de.twomartens.timetable.bahnApi.model.Eva import de.twomartens.timetable.bahnApi.service.BahnApiService -import de.twomartens.timetable.bahnApi.service.BahnDatabaseService -import de.twomartens.timetable.model.Eva -import de.twomartens.timetable.model.Hour +import de.twomartens.timetable.bahnApi.service.TaskScheduler +import de.twomartens.timetable.model.base.HourAtDay import mu.KotlinLogging import org.springframework.scheduling.annotation.Async -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor -import java.time.LocalDate @Async open class FetchTimetableTask( private val eva: Eva, - private val date: LocalDate, - private val hour: Hour, + private val hourAtDay: HourAtDay, private val bahnApiService: BahnApiService, - private val bahnDatabaseService: BahnDatabaseService, - private val executor: ThreadPoolTaskExecutor) : Runnable { + private val scheduler: TaskScheduler) : Runnable { override fun run() { log.info { - "Fetch timetable: [eva: $eva], [date: $date], [hour: $hour]" + "Fetch timetable: [eva: $eva], [date: ${hourAtDay.date}], [hour: ${hourAtDay.hour}]" } - val timetable = bahnApiService.fetchTimetable(eva, date, hour) - val storeTask = StoreTimetableTask(timetable, date.atTime(hour.value, 0), - bahnDatabaseService) - executor.execute(storeTask) + val timetable = bahnApiService.fetchTimetable(eva, hourAtDay) + scheduler.scheduleStoreTask(timetable, hourAtDay) } companion object { diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/StoreTimetableTask.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/StoreTimetableTask.kt index b2c153a..31dc69e 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/StoreTimetableTask.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/tasks/StoreTimetableTask.kt @@ -2,19 +2,19 @@ package de.twomartens.timetable.bahnApi.tasks import de.twomartens.timetable.bahnApi.model.dto.BahnTimetable import de.twomartens.timetable.bahnApi.service.BahnDatabaseService +import de.twomartens.timetable.model.base.HourAtDay import mu.KotlinLogging import org.springframework.scheduling.annotation.Async -import java.time.LocalDateTime @Async open class StoreTimetableTask( private val timetable: BahnTimetable, - private val dateTime: LocalDateTime, + private val hourAtDay: HourAtDay, private val bahnDatabaseService: BahnDatabaseService ) : Runnable { override fun run() { log.info { "Store timetable: [eva: ${timetable.eva}], station: ${timetable.station}]" } - bahnDatabaseService.storeTimetable(timetable, dateTime) + bahnDatabaseService.storeTimetable(timetable, hourAtDay) } companion object { diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/controller/TimetableController.kt b/module-server/src/main/kotlin/de/twomartens/timetable/controller/TimetableController.kt index 935bfc3..e0e05fa 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/controller/TimetableController.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/controller/TimetableController.kt @@ -1,8 +1,9 @@ package de.twomartens.timetable.controller -import de.twomartens.timetable.model.NonEmptyString +import de.twomartens.timetable.bahnApi.service.ScheduledTaskService +import de.twomartens.timetable.model.base.NonEmptyString +import de.twomartens.timetable.model.db.UserId import de.twomartens.timetable.repository.TswRouteRepository -import de.twomartens.timetable.service.ScheduledTaskService import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.responses.ApiResponse @@ -34,8 +35,11 @@ class TimetableController( description = "Request does not follow specification" )] ) - @PostMapping("/{routeName}/fetchTimetable/{date}") + @PostMapping("/{userId}/{routeName}/fetchTimetable/{date}") fun scheduleFetch( + @PathVariable @Parameter(description = "The id of the user", + example = "1", + required = true) userId: UserId, @PathVariable @Parameter(description = "The name of the TSW route.", example = "CologneAachen", required = true) routeName: NonEmptyString, @@ -47,7 +51,7 @@ class TimetableController( if (!date.isAfter(LocalDate.now(clock))) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Date must be in the future") } - val route = routeRepository.findByName(routeName) + val route = routeRepository.findByUserIdAndName(userId, routeName) ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Route name must belong to existing route") scheduledTaskService.scheduleTimetableFetch(route, date) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/Eva.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/Eva.kt deleted file mode 100644 index 7b946e5..0000000 --- a/module-server/src/main/kotlin/de/twomartens/timetable/model/Eva.kt +++ /dev/null @@ -1,10 +0,0 @@ -package de.twomartens.timetable.model - -@JvmInline -value class Eva(val value: Int) { - init { - require(value in 8000000..8999999) { - "Eva number must start with an 8 and have 7 digits" - } - } -} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/Hour.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/Hour.kt deleted file mode 100644 index 7a788d6..0000000 --- a/module-server/src/main/kotlin/de/twomartens/timetable/model/Hour.kt +++ /dev/null @@ -1,10 +0,0 @@ -package de.twomartens.timetable.model - -@JvmInline -value class Hour(val value: Int) { - init { - require(value in 0..23) { - "Hour must be between 0 and 23" - } - } -} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/HourAtDay.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/HourAtDay.kt deleted file mode 100644 index 5bce382..0000000 --- a/module-server/src/main/kotlin/de/twomartens/timetable/model/HourAtDay.kt +++ /dev/null @@ -1,12 +0,0 @@ -package de.twomartens.timetable.model - -import java.time.LocalDateTime - -@JvmInline -value class HourAtDay(val value: LocalDateTime) { - init { - require(value.minute == 0 && value.second == 0 && value.nano == 0) { - "DateTime must only include an hour and the date." - } - } -} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/Tasks.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/Tasks.kt deleted file mode 100644 index 83d323e..0000000 --- a/module-server/src/main/kotlin/de/twomartens/timetable/model/Tasks.kt +++ /dev/null @@ -1,42 +0,0 @@ -package de.twomartens.timetable.model - -import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask -import java.time.LocalDate -import java.time.temporal.ChronoUnit - -private const val SECONDS_BETWEEN_TASKS = 2 - -class Tasks { - private val _tasks: MutableMap> = mutableMapOf() - var tasks: List = listOf() - get() { - return _tasks.flatMap { - it.value - } - } - set(newTasks) { - _tasks.clear() - newTasks.forEach { addTask(it) } - field = newTasks - } - - fun createAndAddTask(eva: Eva, date: LocalDate, hour: Hour): ScheduledFetchTask { - val hourAtDay = HourAtDay(date.atTime(hour.value, 0)) - val tasksForHourAtDay = _tasks.computeIfAbsent(hourAtDay) { mutableListOf() } - val secondInHourToExecute = tasksForHourAtDay.size * SECONDS_BETWEEN_TASKS - val fetchDateTime = hourAtDay.value.plusSeconds(secondInHourToExecute.toLong()) - val task = ScheduledFetchTask(eva, date, hour, fetchDateTime) - - tasksForHourAtDay.add(task) - - return task - } - - private fun addTask(scheduledFetchTask: ScheduledFetchTask) { - val scheduledTime = scheduledFetchTask.scheduledExecutionDateTime - val hourAtDay = HourAtDay(scheduledTime.truncatedTo(ChronoUnit.HOURS)) - val tasksForHourAtDay = _tasks.computeIfAbsent(hourAtDay) { mutableListOf() } - - tasksForHourAtDay.add(scheduledFetchTask) - } -} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/base/Hour.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/base/Hour.kt new file mode 100644 index 0000000..8b252b4 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/base/Hour.kt @@ -0,0 +1,16 @@ +package de.twomartens.timetable.model.base + +@JvmInline +value class Hour private constructor(val value: Int) { + init { + require(value in 0..23) { + "Hour must be between 0 and 23" + } + } + + companion object { + fun of(hour: Int): Hour { + return Hour(hour) + } + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/base/HourAtDay.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/base/HourAtDay.kt new file mode 100644 index 0000000..65e5283 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/base/HourAtDay.kt @@ -0,0 +1,39 @@ +package de.twomartens.timetable.model.base + +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +@JvmInline +value class HourAtDay private constructor(private val value: LocalDateTime) { + init { + require(value.minute == 0 && value.second == 0 && value.nano == 0) { + "DateTime must only include an hour and the date." + } + } + + val date: LocalDate + get() { + return value.toLocalDate() + } + + val hour: Hour + get() { + return Hour.of(value.hour) + } + + val dateTime: LocalDateTime + get() { + return value + } + + companion object { + fun of(dateTime: LocalDateTime): HourAtDay { + return HourAtDay(dateTime.truncatedTo(ChronoUnit.HOURS)) + } + + fun of(hour: Hour, date: LocalDate): HourAtDay { + return HourAtDay(date.atTime(hour.value, 0)) + } + } +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/NonEmptyString.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/base/NonEmptyString.kt similarity index 78% rename from module-server/src/main/kotlin/de/twomartens/timetable/model/NonEmptyString.kt rename to module-server/src/main/kotlin/de/twomartens/timetable/model/base/NonEmptyString.kt index f30dfc5..379a8e7 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/model/NonEmptyString.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/base/NonEmptyString.kt @@ -1,4 +1,4 @@ -package de.twomartens.timetable.model +package de.twomartens.timetable.model.base @JvmInline value class NonEmptyString(val name: String) { diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/base/PositiveInteger.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/base/PositiveInteger.kt new file mode 100644 index 0000000..b25bfb3 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/base/PositiveInteger.kt @@ -0,0 +1,10 @@ +package de.twomartens.timetable.model.base + +@JvmInline +value class PositiveInteger(val value: Int) { + init { + require(value > 0) { + "Value must be positive integer" + } + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/CoachCapacity.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/CoachCapacity.kt new file mode 100644 index 0000000..df5ed1e --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/CoachCapacity.kt @@ -0,0 +1,6 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.PositiveInteger + +@JvmInline +value class CoachCapacity(val capacity: PositiveInteger) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/CountryCode.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/CountryCode.kt new file mode 100644 index 0000000..ffe671c --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/CountryCode.kt @@ -0,0 +1,12 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.NonEmptyString + +@JvmInline +value class CountryCode(val countryCode: NonEmptyString) { + init { + require(countryCode.name.length == 2) { + "CountryCode must be only two characters long to conform to ISO 3166-1 alpha-2" + } + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Depot.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Depot.kt new file mode 100644 index 0000000..bca4ef9 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Depot.kt @@ -0,0 +1,11 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.NonEmptyString +import kotlin.time.Duration + +data class Depot( + var name: NonEmptyString, + var nearestStationId: StationId, + var tracks: Map, + var formationTimes: Map +) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Formation.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Formation.kt new file mode 100644 index 0000000..9866d20 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Formation.kt @@ -0,0 +1,27 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.NonEmptyString +import org.bson.types.ObjectId +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.mongodb.core.index.CompoundIndex +import org.springframework.data.mongodb.core.mapping.Document +import java.time.Instant + +@Document +@CompoundIndex(def = "{'userId': 1, 'formationId': 1}", unique = true) +data class Formation( + var userId: UserId, + var positiveInteger: FormationId, + var name: NonEmptyString +) { + @Id + var id: ObjectId = ObjectId() + + @CreatedDate + lateinit var created: Instant + + @LastModifiedDate + lateinit var lastModified: Instant +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/FormationId.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/FormationId.kt new file mode 100644 index 0000000..61ffd89 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/FormationId.kt @@ -0,0 +1,6 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.PositiveInteger + +@JvmInline +value class FormationId(val id: PositiveInteger) \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Portal.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Portal.kt new file mode 100644 index 0000000..3a7232e --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Portal.kt @@ -0,0 +1,10 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.NonEmptyString +import kotlin.time.Duration + +data class Portal( + var name: NonEmptyString, + var nearestStationId: StationId, + var formationTimes: Map +) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Section.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Section.kt index 6cb33b9..2c18c43 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Section.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Section.kt @@ -1,5 +1,6 @@ package de.twomartens.timetable.model.db -data class Section( - var name: String -) +import de.twomartens.timetable.model.base.NonEmptyString + +@JvmInline +value class Section(val name: NonEmptyString) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Station.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Station.kt index b08bdb2..70d5af1 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Station.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/Station.kt @@ -1,6 +1,5 @@ package de.twomartens.timetable.model.db -import de.twomartens.timetable.model.Eva import org.bson.types.ObjectId import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.Id @@ -11,7 +10,7 @@ import java.time.Instant @Document data class Station( - @Indexed(unique = true) var eva: Eva, + @Indexed(unique = true) var stationId: StationId, var name: String, var platforms: List ) { diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/StationId.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/StationId.kt new file mode 100644 index 0000000..e34c48c --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/StationId.kt @@ -0,0 +1,29 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.NonEmptyString + +@JvmInline +value class StationId(val id: NonEmptyString) { + init { + require(idPattern.matches(id.name)) { + "StationId must start with two character country code, followed by a dash, " + + "and at least one word character" + } + } + + companion object { + private val idPattern = Regex("^\\w{2}-(?\\w.*)") + } + + val stationIdWithinCountry: NonEmptyString + get() { + val matchResult = idPattern.matchEntire(id.name) + val groupMatch = matchResult?.groups?.get("countryStationId")?.value + return when { + matchResult == null || groupMatch == null -> + throw IllegalArgumentException("id wasn't matched by pattern") + + else -> NonEmptyString(groupMatch) + } + } +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TrackId.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TrackId.kt new file mode 100644 index 0000000..2a3017a --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TrackId.kt @@ -0,0 +1,6 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.PositiveInteger + +@JvmInline +value class TrackId(val id: PositiveInteger) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TswRoute.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TswRoute.kt index cd20405..9e0eb69 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TswRoute.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/TswRoute.kt @@ -1,30 +1,23 @@ package de.twomartens.timetable.model.db -import de.twomartens.timetable.model.Eva -import de.twomartens.timetable.model.NonEmptyString +import de.twomartens.timetable.model.base.NonEmptyString import org.bson.types.ObjectId import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.Id import org.springframework.data.annotation.LastModifiedDate -import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.index.CompoundIndex import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant @Document +@CompoundIndex(def = "{'userId': 1, 'name': 1}", unique = true) data class TswRoute( - @Indexed(unique = true) var name: NonEmptyString, - /** - * Ordered list of stations that represent the order of stations on the route. - * - * Not every stop is necessarily called at by each train. There may be multiple - * parallel paths through a route (like S-Bahn track and long-distance track). - * In such cases, the S-Bahn stops are listed as part of the route as they appear - * geographically. - * - * Only the station ids are used here as the duplication would be heavy and - * consistency hard to ensure when potentially repeated across various routes. - */ - var stations: List + var userId: UserId, + var name: NonEmptyString, + var countryCode: CountryCode, + var stationIds: List, + var portals: List, + var depots: List ) { @Id var id: ObjectId = ObjectId() diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/User.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/User.kt new file mode 100644 index 0000000..843b630 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/User.kt @@ -0,0 +1,24 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.NonEmptyString +import org.bson.types.ObjectId +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.mongodb.core.mapping.Document +import java.time.Instant + +@Document +data class User( + var userId: UserId, + var name: NonEmptyString +) { + @Id + var id: ObjectId = ObjectId() + + @CreatedDate + lateinit var created: Instant + + @LastModifiedDate + lateinit var lastModified: Instant +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/model/db/UserId.kt b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/UserId.kt new file mode 100644 index 0000000..3776a5b --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/model/db/UserId.kt @@ -0,0 +1,6 @@ +package de.twomartens.timetable.model.db + +import de.twomartens.timetable.model.base.NonEmptyString + +@JvmInline +value class UserId(val id: NonEmptyString) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/repository/TswRouteRepository.kt b/module-server/src/main/kotlin/de/twomartens/timetable/repository/TswRouteRepository.kt index ddc0ccf..eeaab46 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/repository/TswRouteRepository.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/repository/TswRouteRepository.kt @@ -1,10 +1,11 @@ package de.twomartens.timetable.repository -import de.twomartens.timetable.model.NonEmptyString +import de.twomartens.timetable.model.base.NonEmptyString import de.twomartens.timetable.model.db.TswRoute +import de.twomartens.timetable.model.db.UserId import org.bson.types.ObjectId import org.springframework.data.mongodb.repository.MongoRepository interface TswRouteRepository : MongoRepository { - fun findByName(name: NonEmptyString): TswRoute? + fun findByUserIdAndName(userId: UserId, name: NonEmptyString): TswRoute? } diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/service/ScheduledTaskService.kt b/module-server/src/main/kotlin/de/twomartens/timetable/service/ScheduledTaskService.kt deleted file mode 100644 index bb0d431..0000000 --- a/module-server/src/main/kotlin/de/twomartens/timetable/service/ScheduledTaskService.kt +++ /dev/null @@ -1,77 +0,0 @@ -package de.twomartens.timetable.service - -import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask -import de.twomartens.timetable.bahnApi.repository.ScheduledFetchTaskRepository -import de.twomartens.timetable.bahnApi.service.BahnApiService -import de.twomartens.timetable.bahnApi.service.BahnDatabaseService -import de.twomartens.timetable.bahnApi.tasks.DeleteScheduledTask -import de.twomartens.timetable.bahnApi.tasks.FetchTimetableTask -import de.twomartens.timetable.model.Hour -import de.twomartens.timetable.model.Tasks -import de.twomartens.timetable.model.db.TswRoute -import de.twomartens.timetable.property.TimeProperties -import mu.KotlinLogging -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler -import org.springframework.stereotype.Service -import java.time.LocalDate - -@Service -class ScheduledTaskService( - private val timeProperties: TimeProperties, - private val scheduledFetchTaskRepository: ScheduledFetchTaskRepository, - private val threadPoolTaskScheduler: ThreadPoolTaskScheduler, - private val bahnApiService: BahnApiService, - private val bahnDatabaseService: BahnDatabaseService, - private val threadPoolTaskExecutor: ThreadPoolTaskExecutor -) { - private val tasks: Tasks = Tasks() - - fun initializeScheduledTasks() { - log.info { "Initialize persisted scheduled fetch tasks" } - val scheduledTasks = scheduledFetchTaskRepository.findAll() - tasks.tasks = scheduledTasks - scheduleTasks(scheduledTasks) - } - - fun scheduleTimetableFetch(tswRoute: TswRoute, fetchedDate: LocalDate) { - log.info { - "Schedule timetable fetch: [route ${tswRoute.name}]" - } - val previousDay = fetchedDate.minusDays(1) - val nextDay = fetchedDate.plusDays(1) - val newTasks = mutableListOf() - tswRoute.stations.forEach { - val eva = it - var newTask = tasks.createAndAddTask(eva, date = previousDay, hour = Hour(23)) - newTasks.add(newTask) - for (hour in 0..23) { - newTask = tasks.createAndAddTask(eva, date = fetchedDate, hour = Hour(hour)) - newTasks.add(newTask) - } - for (hour in 0..3) { - newTask = tasks.createAndAddTask(eva, date = nextDay, hour = Hour(hour)) - newTasks.add(newTask) - } - } - - scheduledFetchTaskRepository.saveAll(newTasks) - scheduleTasks(newTasks) - } - - private fun scheduleTasks(scheduledTasks: List) { - scheduledTasks.forEach { - val zonedExecutionTime = it.scheduledExecutionDateTime - .atZone(timeProperties.defaultTimeZone) - val deleteTask = DeleteScheduledTask(scheduledFetchTaskRepository, it) - val timetableTask = FetchTimetableTask(it.eva, it.fetchedDate, it.fetchedTime, - bahnApiService, bahnDatabaseService, threadPoolTaskExecutor) - threadPoolTaskScheduler.schedule(timetableTask, zonedExecutionTime.toInstant()) - threadPoolTaskScheduler.schedule(deleteTask, zonedExecutionTime.toInstant()) - } - } - - companion object { - private val log = KotlinLogging.logger {} - } -} \ No newline at end of file