Follow clean code more closely

This commit is contained in:
2023-10-26 09:26:58 +02:00
parent 06ad9eaa31
commit 0ea147fa8e
47 changed files with 508 additions and 251 deletions
@@ -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
@@ -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()
@@ -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
@@ -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<String?, LocalDateTime>() {
@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)
}
@@ -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
)
}
@@ -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())
}
}
}
@@ -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)
@@ -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<HourAtDay, Int> = mutableMapOf()
fun initializeWithExistingTasks(existingTasks: List<ScheduledFetchTask>) {
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 }
}
@@ -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
@@ -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
@@ -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
@@ -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)
}
@@ -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)
}
@@ -10,5 +10,5 @@ import jakarta.xml.bind.annotation.XmlRootElement
data class BahnStations(
@field:XmlElement(name = "station") var stations: List<BahnStation>
) {
constructor(): this(mutableListOf())
constructor() : this(listOf())
}
@@ -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<BahnStationStop>
@field:XmlAttribute var eva: Eva,
@field:XmlAttribute var station: String,
@field:XmlElement(name = "s") var stops: List<BahnStationStop>
) {
constructor(): this(null, "", mutableListOf())
constructor() : this(Eva.UNKNOWN, "", listOf())
}
@@ -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)
}
@@ -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<BahnTimetable>()
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
@@ -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))
}
}
@@ -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<ScheduledFetchTask> {
val newTasks = mutableListOf<ScheduledFetchTask>()
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<ScheduledFetchTask>) {
scheduledFetchTaskRepository.saveAll(newTasks)
}
companion object {
private val log = KotlinLogging.logger {}
}
}
@@ -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<ScheduledFetchTask>) {
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())
}
}
@@ -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)
}
@@ -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 {
@@ -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 {
@@ -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)
@@ -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"
}
}
}
@@ -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"
}
}
}
@@ -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."
}
}
}
@@ -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<HourAtDay, MutableList<ScheduledFetchTask>> = mutableMapOf()
var tasks: List<ScheduledFetchTask> = 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)
}
}
@@ -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)
}
}
}
@@ -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))
}
}
}
@@ -1,4 +1,4 @@
package de.twomartens.timetable.model
package de.twomartens.timetable.model.base
@JvmInline
value class NonEmptyString(val name: String) {
@@ -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"
}
}
}
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.base.PositiveInteger
@JvmInline
value class CoachCapacity(val capacity: PositiveInteger)
@@ -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"
}
}
}
@@ -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<TrackId, CoachCapacity>,
var formationTimes: Map<FormationId, Duration>
)
@@ -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
}
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.base.PositiveInteger
@JvmInline
value class FormationId(val id: PositiveInteger)
@@ -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<FormationId, Duration>
)
@@ -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)
@@ -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<Platform>
) {
@@ -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}-(?<countryStationId>\\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)
}
}
}
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.base.PositiveInteger
@JvmInline
value class TrackId(val id: PositiveInteger)
@@ -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<Eva>
var userId: UserId,
var name: NonEmptyString,
var countryCode: CountryCode,
var stationIds: List<StationId>,
var portals: List<Portal>,
var depots: List<Depot>
) {
@Id
var id: ObjectId = ObjectId()
@@ -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
}
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.base.NonEmptyString
@JvmInline
value class UserId(val id: NonEmptyString)
@@ -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<TswRoute, ObjectId> {
fun findByName(name: NonEmptyString): TswRoute?
fun findByUserIdAndName(userId: UserId, name: NonEmptyString): TswRoute?
}
@@ -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<ScheduledFetchTask>()
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<ScheduledFetchTask>) {
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 {}
}
}