mirror of
https://github.com/2martens/tsw-timetable.git
synced 2026-10-17 07:57:55 +02:00
feat: Support CRUD functionality (#7)
The station search implementation only works via DB and is very slow. For a simple query, the answer takes about 1s or longer. That is 100 times slower than it should be. For now, however, this solution is adequate to achieve a first prototype that includes the core functionality. feat: Add station search
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
plugins {
|
||||
id("twomartens.spring-boot-cloud")
|
||||
id("twomartens.spring-boot-cloud-application")
|
||||
id("twomartens.kotlin")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":bahnApi"))
|
||||
implementation(project(":common"))
|
||||
implementation(libs.mapstruct.base)
|
||||
implementation(libs.bundles.spring.boot.security)
|
||||
implementation(project(":model"))
|
||||
implementation(project(":support"))
|
||||
|
||||
implementation(libs.jaxb.impl)
|
||||
implementation(libs.jakarta.xml.binding)
|
||||
|
||||
implementation(libs.mapstruct.base)
|
||||
annotationProcessor(libs.mapstruct.processor)
|
||||
kapt(libs.mapstruct.processor)
|
||||
|
||||
implementation(libs.bundles.spring.boot.security)
|
||||
implementation(libs.spring.cloud.starter.config)
|
||||
implementation(libs.spring.cloud.leader.election)
|
||||
implementation(libs.spring.cloud.starter.bus.kafka)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.twomartens.timetable
|
||||
|
||||
import de.twomartens.timetable.support.model.LeadershipStatus
|
||||
import de.twomartens.support.model.LeadershipStatus
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
@@ -13,7 +13,7 @@ import org.springframework.scheduling.annotation.EnableScheduling
|
||||
|
||||
@EnableMongoAuditing
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(scanBasePackages = ["de.twomartens.support", "de.twomartens.timetable"])
|
||||
open class MainApplication(
|
||||
private val leadershipStatus: LeadershipStatus,
|
||||
private val leaderProperties: LeaderProperties
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.twomartens.timetable.auth
|
||||
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.dto.User
|
||||
import de.twomartens.timetable.types.Email
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.mapstruct.factory.Mappers
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/v1/users"])
|
||||
@Tag(name = "Users", description = "all requests relating to user resources")
|
||||
class UserController(
|
||||
private val userRepository: UserRepository
|
||||
) {
|
||||
|
||||
private val mapper = Mappers.getMapper(UserMapper::class.java)
|
||||
|
||||
@Operation(
|
||||
summary = "Store user",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "User was updated",
|
||||
content = [Content(
|
||||
schema = Schema(implementation = User::class)
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "User was created",
|
||||
content = [Content(
|
||||
schema = Schema(implementation = User::class)
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@PutMapping("/{userId}")
|
||||
fun putUser(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@RequestBody(required = true) @io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
required = true,
|
||||
content = [
|
||||
Content(
|
||||
schema = Schema(implementation = User::class)
|
||||
)
|
||||
]) body: User
|
||||
): ResponseEntity<User> {
|
||||
var created = false
|
||||
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
var user = userRepository.findByUserId(userIdConverted)
|
||||
if (user == null) {
|
||||
created = true
|
||||
user = mapper.mapToDB(body)
|
||||
} else {
|
||||
user.name = NonEmptyString(body.name)
|
||||
user.email = Email.of(NonEmptyString(body.email))
|
||||
}
|
||||
|
||||
userRepository.save(user)
|
||||
|
||||
val updatedUser = mapper.mapToDto(user)
|
||||
return if (created) {
|
||||
ResponseEntity.status(HttpStatus.CREATED).body(updatedUser)
|
||||
} else {
|
||||
ResponseEntity.ok(updatedUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.twomartens.timetable.auth
|
||||
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.dto.User
|
||||
import de.twomartens.timetable.types.Email
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import org.mapstruct.*
|
||||
|
||||
@Mapper(
|
||||
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
interface UserMapper {
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "lastModified", ignore = true)
|
||||
fun mapToDB(dto: User): de.twomartens.timetable.model.db.User {
|
||||
return de.twomartens.timetable.model.db.User(
|
||||
UserId.of(NonEmptyString(dto.id)),
|
||||
NonEmptyString(dto.name),
|
||||
Email.of(NonEmptyString(dto.email))
|
||||
)
|
||||
}
|
||||
|
||||
fun mapToDto(db: de.twomartens.timetable.model.db.User): User {
|
||||
return User(
|
||||
db.userId.value,
|
||||
db.name.value,
|
||||
db.email.value
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.twomartens.timetable.auth
|
||||
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.db.User
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
|
||||
interface UserRepository : MongoRepository<User, ObjectId> {
|
||||
fun existsByUserId(userId: UserId): Boolean
|
||||
fun findByUserId(userId: UserId): User?
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.configuration
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
|
||||
|
||||
private const val CORE_POOL_SIZE = 1
|
||||
|
||||
@Configuration
|
||||
open class ThreadPoolTaskExecutorConfig {
|
||||
|
||||
@Bean
|
||||
open fun threadPoolTaskExecutor(): ThreadPoolTaskExecutor {
|
||||
val executor = ThreadPoolTaskExecutor()
|
||||
executor.corePoolSize = CORE_POOL_SIZE
|
||||
executor.maxPoolSize = CORE_POOL_SIZE
|
||||
return executor
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
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
|
||||
open class ThreadPoolTaskSchedulerConfig {
|
||||
@Bean
|
||||
open fun threadPoolTaskScheduler(): ThreadPoolTaskScheduler {
|
||||
val scheduler = ThreadPoolTaskScheduler()
|
||||
scheduler.poolSize = POOL_SIZE
|
||||
scheduler.threadNamePrefix = THREAD_NAME_PREFIX
|
||||
return scheduler
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.events
|
||||
|
||||
import org.springframework.cloud.bus.event.Destination
|
||||
import org.springframework.cloud.bus.event.PathDestinationFactory
|
||||
import org.springframework.cloud.bus.event.RemoteApplicationEvent
|
||||
import java.time.Instant
|
||||
|
||||
private const val TIMETABLE_SERVICE_NAME = "timetable"
|
||||
|
||||
class ScheduledTasksCreatedEvent private constructor(source: Instant, originService: String,
|
||||
destination: Destination)
|
||||
: RemoteApplicationEvent(source, originService, destination) {
|
||||
|
||||
private val creationTime: Instant
|
||||
|
||||
init {
|
||||
creationTime = source
|
||||
}
|
||||
|
||||
override fun getSource(): Instant {
|
||||
return creationTime
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val destinationFactory = PathDestinationFactory()
|
||||
|
||||
fun of(source: Instant): ScheduledTasksCreatedEvent {
|
||||
return ScheduledTasksCreatedEvent(source,
|
||||
TIMETABLE_SERVICE_NAME,
|
||||
destinationFactory.getDestination(TIMETABLE_SERVICE_NAME))
|
||||
}
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
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(DateTimeParseException::class)
|
||||
override fun unmarshal(xmlValue: String?): LocalDateTime {
|
||||
return LocalDateTime.parse(xmlValue, dateFormatter.get())
|
||||
}
|
||||
|
||||
@Throws(DateTimeParseException::class)
|
||||
override fun marshal(kotlinValue: LocalDateTime): String? {
|
||||
return dateFormatter.get().format(kotlinValue)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val dateFormatter: ThreadLocal<DateTimeFormatter> = object : ThreadLocal<DateTimeFormatter>() {
|
||||
override fun initialValue(): DateTimeFormatter {
|
||||
return DateTimeFormatter.ofPattern("yyMMddHHmm", Locale.GERMANY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.mapper
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.db.BahnStation
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import org.mapstruct.*
|
||||
|
||||
@Mapper(
|
||||
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
interface BahnStationMapper {
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "lastModified", ignore = true)
|
||||
fun mapToDB(dto: de.twomartens.timetable.bahnApi.model.dto.BahnStation): BahnStation {
|
||||
return BahnStation(
|
||||
dto.eva,
|
||||
NonEmptyString(dto.name),
|
||||
dto.ds100,
|
||||
dto.db
|
||||
)
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.mapper
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.db.BahnTimetable
|
||||
import de.twomartens.timetable.types.HourAtDay
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import org.mapstruct.*
|
||||
|
||||
@Mapper(
|
||||
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
interface BahnTimetableMapper {
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "lastModified", ignore = true)
|
||||
fun mapToDB(dto: de.twomartens.timetable.bahnApi.model.dto.BahnTimetable,
|
||||
hourAtDay: HourAtDay): BahnTimetable {
|
||||
return BahnTimetable(
|
||||
dto.eva,
|
||||
hourAtDay,
|
||||
NonEmptyString(dto.station),
|
||||
dto.stops
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model
|
||||
|
||||
import de.twomartens.timetable.model.common.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.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
data class FetchDates(val previousDay: LocalDate, val fetchDate: LocalDate, val nextDate: LocalDate)
|
||||
@@ -1,65 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask
|
||||
import de.twomartens.timetable.types.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 updateCounterWith(existingTasks: List<ScheduledFetchTask>) {
|
||||
countTasks(existingTasks)
|
||||
}
|
||||
|
||||
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 countTasks(existingTasks: List<ScheduledFetchTask>) {
|
||||
existingTasks.forEach { countTask(it) }
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
private fun resetCounter() {
|
||||
_tasksPerHour.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.db
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.Eva
|
||||
import de.twomartens.timetable.types.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.TextIndexed
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.time.Instant
|
||||
|
||||
@Document
|
||||
data class BahnStation(
|
||||
@Indexed(unique = true) var eva: Eva,
|
||||
@TextIndexed var name: NonEmptyString,
|
||||
var ds100: String,
|
||||
var db: Boolean
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
||||
|
||||
@CreatedDate
|
||||
lateinit var created: Instant
|
||||
|
||||
@LastModifiedDate
|
||||
lateinit var lastModified: Instant
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
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.types.HourAtDay
|
||||
import de.twomartens.timetable.types.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.index.TextIndexed
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.time.Instant
|
||||
|
||||
@Document
|
||||
@CompoundIndex(def = "{'eva': 1, 'dateTime': 1}", unique = true)
|
||||
data class BahnTimetable(
|
||||
var eva: Eva,
|
||||
var hourAtDay: HourAtDay,
|
||||
@TextIndexed var station: NonEmptyString,
|
||||
var stops: List<BahnStationStop>
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
||||
|
||||
@CreatedDate
|
||||
lateinit var created: Instant
|
||||
|
||||
@LastModifiedDate
|
||||
lateinit var lastModified: Instant
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.db
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.Eva
|
||||
import de.twomartens.timetable.types.HourAtDay
|
||||
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
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Document
|
||||
@CompoundIndex(def = "{'eva': 1, 'fetchedDateTime': 1", unique = true)
|
||||
data class ScheduledFetchTask(
|
||||
var eva: Eva,
|
||||
var fetchedDateTime: HourAtDay,
|
||||
var scheduledExecutionDateTime: LocalDateTime
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
||||
|
||||
@CreatedDate
|
||||
lateinit var created: Instant
|
||||
|
||||
@LastModifiedDate
|
||||
lateinit var lastModified: Instant
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.dto
|
||||
|
||||
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
|
||||
) {
|
||||
constructor() : this("", Eva.UNKNOWN, "", true)
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.dto
|
||||
|
||||
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
|
||||
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?
|
||||
) {
|
||||
constructor() : this(Eva.UNKNOWN, "", BahnTripLabel(), null, null)
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.dto
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlAccessType
|
||||
import jakarta.xml.bind.annotation.XmlAccessorType
|
||||
import jakarta.xml.bind.annotation.XmlElement
|
||||
import jakarta.xml.bind.annotation.XmlRootElement
|
||||
|
||||
@XmlRootElement(name = "stations")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
data class BahnStations(
|
||||
@field:XmlElement(name = "station") var stations: List<BahnStation>
|
||||
) {
|
||||
constructor() : this(listOf())
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.dto
|
||||
|
||||
import de.twomartens.timetable.bahnApi.jaxb.LocalDateTimeAdapter
|
||||
import jakarta.xml.bind.annotation.XmlAccessType
|
||||
import jakarta.xml.bind.annotation.XmlAccessorType
|
||||
import jakarta.xml.bind.annotation.XmlAttribute
|
||||
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
data class BahnStopEvent(
|
||||
@field:XmlAttribute(name = "pde") var plannedDistantEndpoint: String,
|
||||
@field:XmlAttribute(name = "cde") var changedDistantEndpoint: String,
|
||||
@field:XmlAttribute(name = "pt")
|
||||
@field:XmlJavaTypeAdapter(value = LocalDateTimeAdapter::class)
|
||||
var plannedTime: LocalDateTime?,
|
||||
@field:XmlAttribute(name = "ct")
|
||||
@field:XmlJavaTypeAdapter(value = LocalDateTimeAdapter::class)
|
||||
var changedTime: LocalDateTime?,
|
||||
@field:XmlAttribute(name = "ppth") var plannedPath: String,
|
||||
@field:XmlAttribute(name = "cpth") var changedPath: String,
|
||||
@field:XmlAttribute(name = "pp") var plannedPlatform: String,
|
||||
@field:XmlAttribute(name = "cp") var changedPlatform: String,
|
||||
@field:XmlAttribute(name = "wings") var wings: String,
|
||||
@field:XmlAttribute(name = "l") var line: String
|
||||
) {
|
||||
constructor() : this("", "",
|
||||
null,
|
||||
null,
|
||||
"", "", "", "",
|
||||
"", "")
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.dto
|
||||
|
||||
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>
|
||||
) {
|
||||
constructor() : this(Eva.UNKNOWN, "", listOf())
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.dto
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlAccessType
|
||||
import jakarta.xml.bind.annotation.XmlAccessorType
|
||||
import jakarta.xml.bind.annotation.XmlAttribute
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
data class BahnTripLabel(
|
||||
@field:XmlAttribute(name = "c") var category: String,
|
||||
// S: S-Bahn, N: Nahverkehr, F: Fernverkehr
|
||||
@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
|
||||
) {
|
||||
constructor() : this("", "", "", "", BahnTripType.p)
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.model.dto
|
||||
|
||||
enum class BahnTripType {
|
||||
p, // passenger?
|
||||
e,
|
||||
z,
|
||||
s,
|
||||
h,
|
||||
n
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.property
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope
|
||||
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "de.twomartens.timetable.bahn-api")
|
||||
@Schema(description = "Properties to configure the BahnAPI")
|
||||
data class BahnApiProperties @ConstructorBinding constructor(
|
||||
val clientId: String,
|
||||
val clientSecret: String
|
||||
)
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.repository
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.Eva
|
||||
import de.twomartens.timetable.bahnApi.model.db.BahnStation
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
|
||||
interface BahnStationRepository : MongoRepository<BahnStation, ObjectId> {
|
||||
fun findByEva(eva: Eva): BahnStation?
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.repository
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.Eva
|
||||
import de.twomartens.timetable.bahnApi.model.db.BahnTimetable
|
||||
import de.twomartens.timetable.types.HourAtDay
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
|
||||
// TODO: figure out what ID type should be
|
||||
interface BahnTimetableRepository : MongoRepository<BahnTimetable, ObjectId> {
|
||||
fun findByEvaAndHourAtDay(eva: Eva, hourAtDay: HourAtDay): BahnTimetable?
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.repository
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.Eva
|
||||
import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask
|
||||
import de.twomartens.timetable.types.HourAtDay
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import java.time.Instant
|
||||
|
||||
interface ScheduledFetchTaskRepository : MongoRepository<ScheduledFetchTask, ObjectId> {
|
||||
fun findByEvaAndFetchedDateTime(eva: Eva, fetchedDateTime: HourAtDay): ScheduledFetchTask?
|
||||
|
||||
fun findAllByCreatedAfter(createdAt: Instant): List<ScheduledFetchTask>
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
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.types.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.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Service
|
||||
class BahnApiService(
|
||||
private val restTemplate: RestTemplate,
|
||||
private val properties: BahnApiProperties
|
||||
) {
|
||||
fun fetchStations(pattern: String): List<BahnStation> {
|
||||
val requestEntity = buildRequestEntity<BahnStations>()
|
||||
val response = restTemplate.exchange(
|
||||
"https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1/station/${pattern}",
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
BahnStations::class.java
|
||||
)
|
||||
val body = response.body
|
||||
return body?.stations ?: listOf()
|
||||
}
|
||||
|
||||
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(hourAtDay.hour.value, 0)
|
||||
val response = restTemplate.exchange(
|
||||
"https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1/plan/" +
|
||||
"${eva}/${hourAtDay.date.format(dateFormatter)}/${time.format(timeFormatter)}",
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
BahnTimetable::class.java
|
||||
)
|
||||
return response.body!!
|
||||
}
|
||||
|
||||
private fun <T> buildRequestEntity(): HttpEntity<T> {
|
||||
val headers = HttpHeaders()
|
||||
headers.accept = mutableListOf(MediaType.APPLICATION_XML)
|
||||
headers.contentType = MediaType.APPLICATION_XML
|
||||
headers.set("DB-Client-Id", properties.clientId)
|
||||
headers.set("DB-Api-Key", properties.clientSecret)
|
||||
return HttpEntity<T>(headers)
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.service
|
||||
|
||||
import de.twomartens.timetable.bahnApi.mapper.BahnStationMapper
|
||||
import de.twomartens.timetable.bahnApi.mapper.BahnTimetableMapper
|
||||
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.types.HourAtDay
|
||||
import org.mapstruct.factory.Mappers
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class BahnDatabaseService(
|
||||
private val bahnStationRepository: BahnStationRepository,
|
||||
private val bahnTimetableRepository: BahnTimetableRepository
|
||||
) {
|
||||
private val bahnStationMapper = Mappers.getMapper(BahnStationMapper::class.java)
|
||||
private val bahnTimetableMapper = Mappers.getMapper(BahnTimetableMapper::class.java)
|
||||
|
||||
fun storeStations(stations: List<BahnStation>) {
|
||||
bahnStationRepository.saveAll(stations
|
||||
.map { bahnStationMapper.mapToDB(it) })
|
||||
}
|
||||
|
||||
fun storeTimetable(timetable: BahnTimetable, hourAtDay: HourAtDay) {
|
||||
bahnTimetableRepository.save(bahnTimetableMapper.mapToDB(timetable, hourAtDay))
|
||||
}
|
||||
}
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.service
|
||||
|
||||
import de.twomartens.timetable.bahnApi.events.ScheduledTasksCreatedEvent
|
||||
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.db.TswRoute
|
||||
import de.twomartens.timetable.support.model.LeadershipStatus
|
||||
import de.twomartens.timetable.support.service.BusService
|
||||
import de.twomartens.timetable.types.Hour
|
||||
import de.twomartens.timetable.types.HourAtDay
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.cloud.kubernetes.commons.leader.LeaderProperties
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.integration.leader.event.OnGrantedEvent
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
@Service
|
||||
class ScheduledTaskService(
|
||||
private val busService: BusService,
|
||||
private val leadershipStatus: LeadershipStatus,
|
||||
private val leaderProperties: LeaderProperties,
|
||||
private val scheduledFetchTaskRepository: ScheduledFetchTaskRepository,
|
||||
private val taskFactory: TaskFactory,
|
||||
private val taskScheduler: TaskScheduler
|
||||
) {
|
||||
private var createdTime: Instant = Instant.EPOCH
|
||||
private var lastUpdate: Instant = Instant.EPOCH
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady(event: ApplicationReadyEvent) {
|
||||
if (!leaderProperties.isEnabled) {
|
||||
val updateTime = Instant.ofEpochMilli(event.timestamp)
|
||||
updateTaskCounterAndScheduleTasksIfLeader(updateTime)
|
||||
}
|
||||
}
|
||||
|
||||
@EventListener(OnGrantedEvent::class)
|
||||
fun onLeadershipGranted(event: OnGrantedEvent) {
|
||||
val updateTime = Instant.ofEpochMilli(event.timestamp)
|
||||
updateTaskCounterAndScheduleTasksIfLeader(updateTime)
|
||||
}
|
||||
|
||||
@EventListener(ScheduledTasksCreatedEvent::class)
|
||||
fun onScheduledTasksCreated(event: ScheduledTasksCreatedEvent) {
|
||||
updateTaskCounterAndScheduleTasksIfLeader(event.source)
|
||||
}
|
||||
|
||||
private fun updateTaskCounterAndScheduleTasksIfLeader(updateTime: Instant) {
|
||||
log.info { "Update tasks from database and schedule if leader" }
|
||||
val createdTasks = findTasksCreatedSince(lastUpdate)
|
||||
updateCounterIfNotUpToDate(updateTime, createdTasks)
|
||||
scheduleTasksIfLeader(createdTasks)
|
||||
lastUpdate = updateTime
|
||||
}
|
||||
|
||||
fun triggerTimetableFetch(tswRoute: TswRoute, fetchedDate: LocalDate) {
|
||||
log.info {
|
||||
"Trigger timetable fetch: [route ${tswRoute.name}]"
|
||||
}
|
||||
val fetchDates = calculateDatesToFetch(fetchedDate)
|
||||
val newTasks = buildScheduledTasks(tswRoute, fetchDates)
|
||||
|
||||
storeTasksInDatabaseAndStoreCreationTime(newTasks)
|
||||
publishTasksCreatedEvent()
|
||||
}
|
||||
|
||||
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 scheduleTasksIfLeader(tasksToSchedule: List<ScheduledFetchTask>) {
|
||||
if (leadershipStatus.isLeader) {
|
||||
taskScheduler.scheduleFetchTasks(tasksToSchedule)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCounterIfNotUpToDate(updateTime: Instant,
|
||||
createdTasks: List<ScheduledFetchTask>) {
|
||||
if (notUpToDate(updateTime)) {
|
||||
taskFactory.updateCounterWith(createdTasks)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notUpToDate(updateTime: Instant) = createdTime != updateTime
|
||||
|
||||
private fun findTasksCreatedSince(lastScheduledTime: Instant): List<ScheduledFetchTask> {
|
||||
return scheduledFetchTaskRepository.findAllByCreatedAfter(lastScheduledTime)
|
||||
}
|
||||
|
||||
private fun storeTasksInDatabaseAndStoreCreationTime(newTasks: List<ScheduledFetchTask>) {
|
||||
val savedEntities = scheduledFetchTaskRepository.saveAll(newTasks)
|
||||
storeCreationTime(savedEntities)
|
||||
}
|
||||
|
||||
private fun storeCreationTime(savedEntities: MutableList<ScheduledFetchTask>) {
|
||||
if (savedEntities.isNotEmpty()) {
|
||||
createdTime = savedEntities[savedEntities.lastIndex].created
|
||||
}
|
||||
}
|
||||
|
||||
private fun publishTasksCreatedEvent() {
|
||||
val event = ScheduledTasksCreatedEvent.of(createdTime)
|
||||
busService.publishEvent(event)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
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.types.HourAtDay
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Clock
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
private const val PAST_TASK_EXECUTION_OFFSET = 1
|
||||
|
||||
@Service
|
||||
class TaskScheduler(
|
||||
private val clock: Clock,
|
||||
private val threadPoolTaskScheduler: ThreadPoolTaskScheduler,
|
||||
private val threadPoolTaskExecutor: ThreadPoolTaskExecutor,
|
||||
private val bahnApiService: BahnApiService,
|
||||
private val bahnDatabaseService: BahnDatabaseService,
|
||||
private val scheduledFetchTaskRepository: ScheduledFetchTaskRepository
|
||||
) {
|
||||
fun scheduleStoreTask(timetable: BahnTimetable, hourAtDay: HourAtDay) {
|
||||
val storeTask = StoreTimetableTask(timetable, hourAtDay, bahnDatabaseService)
|
||||
threadPoolTaskExecutor.execute(storeTask)
|
||||
}
|
||||
|
||||
fun scheduleFetchTasks(scheduledTasks: List<ScheduledFetchTask>) {
|
||||
val nowPlus1Minute = ZonedDateTime.now(clock).plusMinutes(1)
|
||||
scheduleTasksFromThePast(scheduledTasks, nowPlus1Minute)
|
||||
scheduleTasksFromTheFuture(scheduledTasks, nowPlus1Minute)
|
||||
}
|
||||
|
||||
private fun scheduleTasksFromThePast(
|
||||
scheduledTasks: List<ScheduledFetchTask>,
|
||||
nowPlus1Minute: ZonedDateTime
|
||||
) {
|
||||
val tasksScheduledForPast = scheduledTasks.filter {
|
||||
val zonedExecutionTime = it.scheduledExecutionDateTime.atZone(clock.zone)
|
||||
nowPlus1Minute.isAfter(zonedExecutionTime)
|
||||
}
|
||||
tasksScheduledForPast.forEach {
|
||||
val zonedExecutionTime = it.scheduledExecutionDateTime.atZone(clock.zone)
|
||||
val adjustedExecutionTime = nowPlus1Minute.withOffsetAndSecondsFrom(zonedExecutionTime)
|
||||
scheduleDeleteTask(it, adjustedExecutionTime)
|
||||
scheduleFetchTimetableTask(it, adjustedExecutionTime)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleTasksFromTheFuture(
|
||||
scheduledTasks: List<ScheduledFetchTask>,
|
||||
nowPlus1Minute: ZonedDateTime
|
||||
) {
|
||||
val tasksScheduledForFuture = scheduledTasks.filter {
|
||||
val zonedExecutionTime = it.scheduledExecutionDateTime.atZone(clock.zone)
|
||||
nowPlus1Minute.isOnOrBefore(zonedExecutionTime)
|
||||
}
|
||||
tasksScheduledForFuture.forEach {
|
||||
val zonedExecutionTime = it.scheduledExecutionDateTime.atZone(clock.zone)
|
||||
scheduleDeleteTask(it, zonedExecutionTime)
|
||||
scheduleFetchTimetableTask(it, zonedExecutionTime)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun ZonedDateTime.withOffsetAndSecondsFrom(other: ZonedDateTime): ZonedDateTime =
|
||||
this.withSecond(other.second + PAST_TASK_EXECUTION_OFFSET)
|
||||
|
||||
fun ZonedDateTime.isOnOrBefore(other: ZonedDateTime): Boolean =
|
||||
this.isBefore(other) || this.isEqual(other)
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package de.twomartens.timetable.bahnApi.tasks
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.db.ScheduledFetchTask
|
||||
import de.twomartens.timetable.bahnApi.repository.ScheduledFetchTaskRepository
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
|
||||
@Async
|
||||
open class DeleteScheduledTask(
|
||||
private val scheduledFetchTaskRepository: ScheduledFetchTaskRepository,
|
||||
private val scheduledFetchTask: ScheduledFetchTask
|
||||
) : Runnable {
|
||||
override fun run() {
|
||||
log.info {
|
||||
"Delete scheduled fetch task after the fetch occurred: " +
|
||||
"[eva: ${scheduledFetchTask.eva}], " +
|
||||
"[date: ${scheduledFetchTask.fetchedDateTime.date}], " +
|
||||
"[time: ${scheduledFetchTask.fetchedDateTime.hour}]"
|
||||
}
|
||||
scheduledFetchTaskRepository.delete(scheduledFetchTask)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
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.TaskScheduler
|
||||
import de.twomartens.timetable.types.HourAtDay
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
|
||||
@Async
|
||||
open class FetchTimetableTask(
|
||||
private val eva: Eva,
|
||||
private val hourAtDay: HourAtDay,
|
||||
private val bahnApiService: BahnApiService,
|
||||
private val scheduler: TaskScheduler) : Runnable {
|
||||
override fun run() {
|
||||
log.info {
|
||||
"Fetch timetable: [eva: $eva], [date: ${hourAtDay.date}], [hour: ${hourAtDay.hour}]"
|
||||
}
|
||||
val timetable = bahnApiService.fetchTimetable(eva, hourAtDay)
|
||||
scheduler.scheduleStoreTask(timetable, hourAtDay)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
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.types.HourAtDay
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
|
||||
@Async
|
||||
open class StoreTimetableTask(
|
||||
private val timetable: BahnTimetable,
|
||||
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, hourAtDay)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
package de.twomartens.timetable.configuration
|
||||
|
||||
import org.springframework.cloud.bus.jackson.RemoteApplicationEventScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@RemoteApplicationEventScan(basePackages = ["de.twomartens.timetable"])
|
||||
@RemoteApplicationEventScan(basePackages = ["de.twomartens.timetable", "de.twomartens.support"])
|
||||
open class BusConfiguration
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
package de.twomartens.timetable.configuration
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
|
||||
import io.swagger.v3.oas.annotations.security.OAuthFlow
|
||||
+5
-3
@@ -1,10 +1,11 @@
|
||||
package de.twomartens.timetable.configuration
|
||||
|
||||
import de.twomartens.support.property.HealthCheckProperties
|
||||
import de.twomartens.support.property.RestTemplateTimeoutProperties
|
||||
import de.twomartens.support.property.StatusProbeProperties
|
||||
import de.twomartens.support.property.TimeProperties
|
||||
import de.twomartens.timetable.bahnApi.property.BahnApiProperties
|
||||
import de.twomartens.timetable.property.RestTemplateTimeoutProperties
|
||||
import de.twomartens.timetable.property.ServiceProperties
|
||||
import de.twomartens.timetable.property.StatusProbeProperties
|
||||
import de.twomartens.timetable.property.TimeProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.cloud.kubernetes.commons.leader.LeaderProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@@ -12,5 +13,6 @@ import org.springframework.context.annotation.Configuration
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(RestTemplateTimeoutProperties::class, ServiceProperties::class,
|
||||
StatusProbeProperties::class, TimeProperties::class, BahnApiProperties::class,
|
||||
HealthCheckProperties::class,
|
||||
LeaderProperties::class)
|
||||
open class PropertyConfiguration
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
package de.twomartens.timetable.configuration
|
||||
|
||||
import de.twomartens.timetable.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.support.interceptor.HeaderInterceptorRest
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
@@ -17,7 +17,8 @@ open class WebConfiguration(private val headerInterceptorRest: HeaderInterceptor
|
||||
val registration = registry.addMapping("/**")
|
||||
registration.allowedMethods(
|
||||
HttpMethod.GET.name(), HttpMethod.POST.name(),
|
||||
HttpMethod.PUT.name(), HttpMethod.HEAD.name()
|
||||
HttpMethod.PUT.name(), HttpMethod.HEAD.name(),
|
||||
HttpMethod.DELETE.name()
|
||||
)
|
||||
registration.allowCredentials(true)
|
||||
registration.allowedOrigins(
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
package de.twomartens.timetable.configuration
|
||||
|
||||
import de.twomartens.timetable.support.security.SpringPolicyEnforcerFilter
|
||||
import de.twomartens.support.security.SpringPolicyEnforcerFilter
|
||||
import org.keycloak.adapters.authorization.spi.ConfigurationResolver
|
||||
import org.keycloak.adapters.authorization.spi.HttpRequest
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
|
||||
@@ -72,10 +72,10 @@ open class WebSecurityConfiguration {
|
||||
|
||||
companion object {
|
||||
private val PERMITTED_PATHS: Collection<String> = listOf(
|
||||
"/timetable/v1/healthCheck",
|
||||
"/timetable/healthCheck",
|
||||
"/actuator/**",
|
||||
"/timetable/v1/doc/**",
|
||||
"/timetable/v1/api-docs/**",
|
||||
"/doc/v1/timetable/**",
|
||||
"/api-docs/v1/timetable/**",
|
||||
"/error",
|
||||
"/timetable/version",
|
||||
)
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package de.twomartens.timetable.controller
|
||||
|
||||
import de.twomartens.timetable.bahnApi.service.ScheduledTaskService
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.repository.TswRouteRepository
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/timetable/v1"])
|
||||
@Tag(name = "Timetables", description = "all requests relating to timetables")
|
||||
class TimetableController(
|
||||
private val clock: Clock,
|
||||
private val routeRepository: TswRouteRepository,
|
||||
private val scheduledTaskService: ScheduledTaskService
|
||||
) {
|
||||
@Operation(
|
||||
summary = "Task the backend with fetching a timetable for specified route and date",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Timetable will be fetched as requested"
|
||||
), ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Request does not follow specification"
|
||||
)]
|
||||
)
|
||||
@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,
|
||||
@PathVariable @Parameter(
|
||||
description = "The date to fetch timetable for. Must be in the future.",
|
||||
example = "2023-09-12",
|
||||
required = true) date: LocalDate
|
||||
) {
|
||||
if (!date.isAfter(LocalDate.now(clock))) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Date must be in the future")
|
||||
}
|
||||
val route = routeRepository.findByUserIdAndName(userId, routeName)
|
||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Route name must belong to existing route")
|
||||
scheduledTaskService.triggerTimetableFetch(route, date)
|
||||
}
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
package de.twomartens.timetable.formation
|
||||
|
||||
import de.twomartens.timetable.auth.UserRepository
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.dto.Formation
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import de.twomartens.timetable.types.ZeroOrPositiveInteger
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.mapstruct.factory.Mappers
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/v1/formations"])
|
||||
@Tag(name = "Formations", description = "all requests relating to formation resources")
|
||||
class FormationController(
|
||||
private val formationRepository: FormationRepository,
|
||||
private val userRepository: UserRepository
|
||||
) {
|
||||
private val mapper = Mappers.getMapper(FormationMapper::class.java)
|
||||
|
||||
@Operation(
|
||||
summary = "Access formations of user and filter by name",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "List of found formations",
|
||||
content = [Content(
|
||||
array = ArraySchema(schema = Schema(implementation = Formation::class))
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@GetMapping("/{userId}/")
|
||||
fun getFormations(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@RequestParam(name = "name", required = false) @Parameter(description = "Searched name",
|
||||
example = "ICE",
|
||||
required = false) name: String?
|
||||
): ResponseEntity<List<Formation>> {
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
val userExists = userRepository.existsByUserId(userIdConverted)
|
||||
if (!userExists) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
val formations = if (!name.isNullOrBlank()) {
|
||||
formationRepository.findAllByUserIdAndNameContainingIgnoreCase(userIdConverted, name)
|
||||
} else {
|
||||
formationRepository.findAllByUserId(userIdConverted)
|
||||
}
|
||||
|
||||
val formationsDto = formations.map {
|
||||
val trainSimWorldFormation = if (it.trainSimWorldFormationId != null) {
|
||||
formationRepository.findByUserIdAndFormationId(userIdConverted, it.trainSimWorldFormationId!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
mapper.mapToDto(it, trainSimWorldFormation)
|
||||
}
|
||||
return ResponseEntity.ok(formationsDto)
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Access formation with id belonging to user",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Formation exists and was returned",
|
||||
content = [Content(
|
||||
schema = Schema(implementation = Formation::class)
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Formation was not found",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@GetMapping("/{userId}/{id}")
|
||||
fun getFormation(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@PathVariable @Parameter(description = "The id of the formation",
|
||||
example = "1",
|
||||
required = true) id: String
|
||||
): ResponseEntity<Formation> {
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
val formationIdConverted = FormationId.of(NonEmptyString(id))
|
||||
val userExists = userRepository.existsByUserId(userIdConverted)
|
||||
if (!userExists) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
val formation = formationRepository.findByUserIdAndFormationId(userIdConverted, formationIdConverted)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
val trainSimWorldFormation = if (formation.trainSimWorldFormationId != null) {
|
||||
formationRepository.findByUserIdAndFormationId(userIdConverted, formation.trainSimWorldFormationId!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return ResponseEntity.ok(mapper.mapToDto(formation, trainSimWorldFormation))
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Store formation with id belonging to user",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Formation was updated",
|
||||
content = [Content(
|
||||
schema = Schema(implementation = Formation::class)
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "Formation was created",
|
||||
content = [Content(
|
||||
schema = Schema(implementation = Formation::class)
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@PutMapping("/{userId}/{id}")
|
||||
fun putFormation(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@PathVariable @Parameter(description = "The id of the formation",
|
||||
example = "1",
|
||||
required = true) id: String,
|
||||
@RequestBody(required = true) @io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
required = true,
|
||||
content = [
|
||||
Content(
|
||||
schema = Schema(implementation = Formation::class)
|
||||
)
|
||||
]) body: Formation
|
||||
): ResponseEntity<Formation> {
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
val formationIdConverted = FormationId.of(NonEmptyString(id))
|
||||
val userExists = userRepository.existsByUserId(userIdConverted)
|
||||
if (!userExists) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
var formation = formationRepository.findByUserIdAndFormationId(userIdConverted, formationIdConverted)
|
||||
val trainSimWorldFormation = createOrUpdateTrainSimWorldFormation(userIdConverted, body.trainSimWorldFormation)
|
||||
if (trainSimWorldFormation != null) {
|
||||
formationRepository.save(trainSimWorldFormation)
|
||||
}
|
||||
|
||||
var created = false
|
||||
|
||||
if (formation == null) {
|
||||
created = true
|
||||
formation = mapper.mapToDB(userIdConverted, body)
|
||||
} else {
|
||||
formation.name = NonEmptyString(body.name)
|
||||
formation.trainSimWorldFormationId = if (body.trainSimWorldFormation != null)
|
||||
FormationId.of(NonEmptyString(body.trainSimWorldFormation!!.id)) else null
|
||||
formation.formation = body.formation
|
||||
formation.length = ZeroOrPositiveInteger(body.length)
|
||||
}
|
||||
|
||||
formationRepository.save(formation)
|
||||
|
||||
val updatedFormation = mapper.mapToDto(formation, trainSimWorldFormation)
|
||||
return if (created) {
|
||||
ResponseEntity.status(HttpStatus.CREATED).body(updatedFormation)
|
||||
} else {
|
||||
ResponseEntity.ok(updatedFormation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createOrUpdateTrainSimWorldFormation(
|
||||
userId: UserId,
|
||||
formation: Formation?
|
||||
): de.twomartens.timetable.model.db.Formation? {
|
||||
if (formation == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var trainSimWorldFormation = formationRepository.findByUserIdAndFormationId(
|
||||
userId, FormationId.of(NonEmptyString(formation.id)))
|
||||
if (trainSimWorldFormation == null) {
|
||||
trainSimWorldFormation = mapper.mapToDB(userId, formation)
|
||||
trainSimWorldFormation.trainSimWorldFormationId = null
|
||||
} else {
|
||||
trainSimWorldFormation.name = NonEmptyString(formation.name)
|
||||
trainSimWorldFormation.trainSimWorldFormationId = null
|
||||
trainSimWorldFormation.formation = formation.formation
|
||||
trainSimWorldFormation.length = ZeroOrPositiveInteger(formation.length)
|
||||
}
|
||||
return trainSimWorldFormation
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Delete formation with id belonging to user",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "Formation was deleted",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
), ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Formation was not found",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@DeleteMapping("/{userId}/{id}")
|
||||
fun deleteFormation(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@PathVariable @Parameter(description = "The id of the formation",
|
||||
example = "1",
|
||||
required = true) id: String,
|
||||
): ResponseEntity<Void> {
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
val formationIdConverted = FormationId.of(NonEmptyString(id))
|
||||
val userExists = userRepository.existsByUserId(userIdConverted)
|
||||
if (!userExists) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
val formation = formationRepository.findByUserIdAndFormationId(userIdConverted, formationIdConverted)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
formationRepository.delete(formation)
|
||||
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.twomartens.timetable.formation
|
||||
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.dto.Formation
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import de.twomartens.timetable.types.ZeroOrPositiveInteger
|
||||
import org.mapstruct.*
|
||||
|
||||
@Mapper(
|
||||
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
interface FormationMapper {
|
||||
|
||||
fun mapToDto(db: de.twomartens.timetable.model.db.Formation,
|
||||
trainSimWorldFormation: de.twomartens.timetable.model.db.Formation?): Formation {
|
||||
|
||||
val dtoTrainSimWorldFormation = if (trainSimWorldFormation != null) {
|
||||
mapToDto(trainSimWorldFormation, null)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return Formation(
|
||||
db.formationId.id,
|
||||
db.name.value,
|
||||
dtoTrainSimWorldFormation,
|
||||
db.formation,
|
||||
db.length.value
|
||||
)
|
||||
}
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "lastModified", ignore = true)
|
||||
fun mapToDB(userId: UserId, dto: Formation): de.twomartens.timetable.model.db.Formation {
|
||||
return de.twomartens.timetable.model.db.Formation(
|
||||
userId,
|
||||
FormationId.of(NonEmptyString(dto.id)),
|
||||
NonEmptyString(dto.name),
|
||||
if (dto.trainSimWorldFormation != null)
|
||||
FormationId.of(NonEmptyString(dto.trainSimWorldFormation!!.id)) else null,
|
||||
dto.formation,
|
||||
ZeroOrPositiveInteger(dto.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package de.twomartens.timetable.formation
|
||||
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.db.Formation
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
|
||||
interface FormationRepository : MongoRepository<Formation, ObjectId> {
|
||||
fun findByUserIdAndFormationId(userId: UserId, formationId: FormationId): Formation?
|
||||
fun findAllByUserId(userId: UserId): List<Formation>
|
||||
fun findAllByUserIdAndNameContainingIgnoreCase(userId: UserId, name: String): List<Formation>
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.PositiveInteger
|
||||
|
||||
@JvmInline
|
||||
value class CoachCapacity(val capacity: PositiveInteger)
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class CountryCode(val countryCode: NonEmptyString) {
|
||||
init {
|
||||
require(countryCode.value.length == 2) {
|
||||
"CountryCode must be only two characters long to conform to ISO 3166-1 alpha-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class Depot(
|
||||
var name: NonEmptyString,
|
||||
var nearestStationId: StationId,
|
||||
var tracks: Map<TrackId, CoachCapacity>,
|
||||
var formationTimes: Map<FormationId, Duration>
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.PositiveInteger
|
||||
|
||||
@JvmInline
|
||||
value class FormationId private constructor(val id: Int) {
|
||||
companion object {
|
||||
val EMPTY = FormationId(-1)
|
||||
|
||||
fun of(id: PositiveInteger): FormationId {
|
||||
return FormationId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
enum class LanguageCode {
|
||||
de,
|
||||
en
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class Line private constructor(val name: String) {
|
||||
companion object {
|
||||
val EMPTY = Line("EMPTY")
|
||||
|
||||
fun of(name: NonEmptyString): Line {
|
||||
return Line(name.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
data class Platform(
|
||||
var name: String,
|
||||
var sections: MutableList<Section>
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class Portal(
|
||||
var name: NonEmptyString,
|
||||
var nearestStationId: StationId,
|
||||
var formationTimes: Map<FormationId, Duration>
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.PositiveInteger
|
||||
|
||||
@JvmInline
|
||||
value class RotationId private constructor(val id: Int) {
|
||||
companion object {
|
||||
fun of(id: PositiveInteger): RotationId {
|
||||
return RotationId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class Section private constructor(val value: String) {
|
||||
companion object {
|
||||
fun of(section: NonEmptyString): Section {
|
||||
return Section(section.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class ServiceId private constructor(val value: String) {
|
||||
companion object {
|
||||
fun of(id: NonEmptyString): ServiceId {
|
||||
return ServiceId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class StationId private constructor(val value: String) {
|
||||
|
||||
companion object {
|
||||
private val idPattern = Regex("^\\w{2}-(?<countryStationId>\\w.*)")
|
||||
|
||||
fun of(id: NonEmptyString): StationId {
|
||||
require(idPattern.matches(id.value)) {
|
||||
"StationId must start with two character country code, followed by a dash, " +
|
||||
"and at least one word character"
|
||||
}
|
||||
|
||||
return StationId(id.value)
|
||||
}
|
||||
}
|
||||
|
||||
val stationIdWithinCountry: String
|
||||
get() {
|
||||
val matchResult = idPattern.matchEntire(value)
|
||||
val groupMatch = matchResult?.groups?.get("countryStationId")?.value
|
||||
|
||||
require(matchResult != null && groupMatch != null)
|
||||
|
||||
return groupMatch
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.PositiveInteger
|
||||
|
||||
@JvmInline
|
||||
value class TrackId private constructor(val value: Int) {
|
||||
companion object {
|
||||
fun of(id: PositiveInteger): TrackId {
|
||||
return TrackId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class UserId private constructor(val value: String) {
|
||||
companion object {
|
||||
fun of(id: NonEmptyString): UserId {
|
||||
return UserId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.types.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 formationId: FormationId,
|
||||
var name: NonEmptyString,
|
||||
var trainSimWorldFormationId: FormationId,
|
||||
var coaches: List<NonEmptyString>
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
||||
|
||||
@CreatedDate
|
||||
lateinit var created: Instant
|
||||
|
||||
@LastModifiedDate
|
||||
lateinit var lastModified: Instant
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.Platform
|
||||
import de.twomartens.timetable.model.common.StationId
|
||||
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.mapping.Document
|
||||
import java.time.Instant
|
||||
|
||||
@Document
|
||||
data class Station(
|
||||
@Indexed(unique = true) var stationId: StationId,
|
||||
var name: String,
|
||||
var platforms: List<Platform>
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
||||
|
||||
@CreatedDate
|
||||
lateinit var created: Instant
|
||||
|
||||
@LastModifiedDate
|
||||
lateinit var lastModified: Instant
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.*
|
||||
import de.twomartens.timetable.types.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, 'name': 1}", unique = true)
|
||||
data class TswRoute(
|
||||
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()
|
||||
|
||||
@CreatedDate
|
||||
lateinit var created: Instant
|
||||
|
||||
@LastModifiedDate
|
||||
lateinit var lastModified: Instant
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.types.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
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.model.common.Line
|
||||
import de.twomartens.timetable.model.common.ServiceId
|
||||
import java.time.LocalTime
|
||||
|
||||
data class AdditionalService(
|
||||
val id: ServiceId,
|
||||
val line: Line,
|
||||
val formationId: FormationId,
|
||||
val direction: Direction,
|
||||
val start: Station,
|
||||
val destination: Station,
|
||||
val serviceStartTime: LocalTime,
|
||||
val stops: List<AdditionalServiceStop>
|
||||
)
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import java.time.LocalTime
|
||||
|
||||
data class AdditionalServiceStop(
|
||||
val stop: Station,
|
||||
val arrivalTime: LocalTime,
|
||||
val departureTime: LocalTime,
|
||||
val platformAndSection: NonEmptyString,
|
||||
val loading: Boolean
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.CountryCode
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
data class Country(
|
||||
val countryCode: CountryCode,
|
||||
val name: NonEmptyString
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
enum class Direction {
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
data class Formation(
|
||||
val id: FormationId,
|
||||
val name: NonEmptyString,
|
||||
val trainSimWorldFormationId: FormationId?,
|
||||
val coaches: List<NonEmptyString>
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.model.common.RotationId
|
||||
import java.time.LocalTime
|
||||
|
||||
data class Rotation(
|
||||
val id: RotationId,
|
||||
val formationId: FormationId,
|
||||
val firstServiceStartTime: LocalTime,
|
||||
val lastServiceEndTime: LocalTime,
|
||||
val startsInVault: Boolean,
|
||||
|
||||
)
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.model.common.Line
|
||||
import de.twomartens.timetable.model.common.ServiceId
|
||||
|
||||
data class ServiceFormationStage(
|
||||
val id: ServiceId,
|
||||
val line: Line,
|
||||
val direction: Direction,
|
||||
val formation: FormationId,
|
||||
val formationReversed: Boolean,
|
||||
val startStop: ServiceStop,
|
||||
val destinationStop: ServiceStop
|
||||
)
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.FormationId
|
||||
import de.twomartens.timetable.model.common.Line
|
||||
import de.twomartens.timetable.model.common.ServiceId
|
||||
|
||||
data class ServiceLinkingStage(
|
||||
val id: ServiceId,
|
||||
val formationId: FormationId,
|
||||
val line: Line,
|
||||
val direction: Direction,
|
||||
val formationReversed: Boolean,
|
||||
val startStop: ServiceStop,
|
||||
val fromDepotOrSidingAtStart: Boolean,
|
||||
val destinationStop: ServiceStop,
|
||||
val fromDepotOrSidingAtDestination: Boolean,
|
||||
)
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.Line
|
||||
import de.twomartens.timetable.model.common.ServiceId
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
data class ServiceRotationStage(
|
||||
val id: ServiceId,
|
||||
val line: Line,
|
||||
val originStop: NonEmptyString,
|
||||
val virtualDestinations: List<NonEmptyString>,
|
||||
val startingFrom: NonEmptyString,
|
||||
val endingIn: NonEmptyString,
|
||||
val serviceStartTime: NonEmptyString,
|
||||
val stops: List<ServiceStop>
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
data class ServiceStop(
|
||||
val stop: Station,
|
||||
val arrivalTime: String,
|
||||
val departureTime: String,
|
||||
val platform: NonEmptyString,
|
||||
val section: String
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.StationId
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
data class Station(
|
||||
val id: StationId,
|
||||
val name: NonEmptyString
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import java.time.LocalDate
|
||||
|
||||
data class Timetable(
|
||||
val name: NonEmptyString,
|
||||
val country: Country,
|
||||
val routeName: NonEmptyString,
|
||||
val fetchDate: LocalDate,
|
||||
val state: TimetableState,
|
||||
val numberOfServices: String
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
enum class TimetableState {
|
||||
NEW,
|
||||
PROCESSING,
|
||||
ENTER_FORMATIONS,
|
||||
LINK_SERVICES,
|
||||
READY_FOR_USAGE
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.Depot
|
||||
import de.twomartens.timetable.model.common.Portal
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
data class TswRoute(
|
||||
val name: NonEmptyString,
|
||||
val country: Country,
|
||||
val stations: List<Station>,
|
||||
val portals: List<Portal>,
|
||||
val depots: List<Depot>
|
||||
)
|
||||
@@ -11,6 +11,5 @@ import org.springframework.cloud.context.config.annotation.RefreshScope
|
||||
@ConfigurationProperties(prefix = "de.twomartens.timetable")
|
||||
@Schema(description = "Properties, to configure this Application")
|
||||
class ServiceProperties {
|
||||
lateinit var greeting: String
|
||||
lateinit var countryNames: Map<CountryCode, Map<LanguageCode, NonEmptyString>>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
package de.twomartens.timetable.route
|
||||
|
||||
import de.twomartens.timetable.auth.UserRepository
|
||||
import de.twomartens.timetable.model.common.RouteId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.dto.TswRoute
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.mapstruct.factory.Mappers
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/v1/routes"])
|
||||
@Tag(name = "Routes", description = "all requests relating to routes")
|
||||
class RouteController(
|
||||
private val routeRepository: TswRouteRepository,
|
||||
private val userRepository: UserRepository
|
||||
) {
|
||||
private val mapper = Mappers.getMapper(RouteMapper::class.java)
|
||||
|
||||
@Operation(
|
||||
summary = "Access routes of user and filter by name",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "List of found routes",
|
||||
content = [Content(
|
||||
array = ArraySchema(schema = Schema(implementation = TswRoute::class))
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@GetMapping("/{userId}/")
|
||||
fun getRoutes(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@RequestParam(name = "name", required = false) @Parameter(description = "Searched name",
|
||||
example = "1",
|
||||
required = false) name: String?
|
||||
): ResponseEntity<List<TswRoute>> {
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
val userExists = userRepository.existsByUserId(userIdConverted)
|
||||
if (!userExists) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
val routes = if (!name.isNullOrBlank()) {
|
||||
routeRepository.findAllByUserIdAndNameContainingIgnoreCase(userIdConverted, name)
|
||||
} else {
|
||||
routeRepository.findAllByUserId(userIdConverted)
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(mapper.mapRoutesToDto(routes))
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Access route with id belonging to user",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Route exists and was returned",
|
||||
content = [Content(
|
||||
schema = Schema(implementation = TswRoute::class)
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Route was not found",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@GetMapping("/{userId}/{id}")
|
||||
fun getRoute(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@PathVariable @Parameter(description = "The id of the route",
|
||||
example = "1",
|
||||
required = true) id: String
|
||||
): ResponseEntity<TswRoute> {
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
val routeIdConverted = RouteId.of(NonEmptyString(id))
|
||||
val userExists = userRepository.existsByUserId(userIdConverted)
|
||||
if (!userExists) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
val route = routeRepository.findByUserIdAndRouteId(userIdConverted, routeIdConverted)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
return ResponseEntity.ok(mapper.mapToDto(route))
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Store route with id belonging to user",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Route was updated",
|
||||
content = [Content(
|
||||
schema = Schema(implementation = TswRoute::class)
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "Route was created",
|
||||
content = [Content(
|
||||
schema = Schema(implementation = TswRoute::class)
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@PutMapping("/{userId}/{id}")
|
||||
fun putRoute(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@PathVariable @Parameter(description = "The id of the route",
|
||||
example = "1",
|
||||
required = true) id: String,
|
||||
@RequestBody(required = true) @io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
required = true,
|
||||
content = [
|
||||
Content(
|
||||
schema = Schema(implementation = TswRoute::class)
|
||||
)
|
||||
]) body: TswRoute
|
||||
): ResponseEntity<TswRoute> {
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
val routeIdConverted = RouteId.of(NonEmptyString(id))
|
||||
val userExists = userRepository.existsByUserId(userIdConverted)
|
||||
if (!userExists) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
var route = routeRepository.findByUserIdAndRouteId(userIdConverted, routeIdConverted)
|
||||
var created = false
|
||||
|
||||
if (route == null) {
|
||||
created = true
|
||||
route = mapper.mapToDB(userIdConverted, body)
|
||||
} else {
|
||||
route.name = NonEmptyString(body.name)
|
||||
route.country = body.country
|
||||
route.stations = body.stations
|
||||
route.depots = mapper.mapDepotsToDB(body.depots)
|
||||
route.portals = mapper.mapPortalsToDB(body.portals)
|
||||
}
|
||||
|
||||
routeRepository.save(route)
|
||||
|
||||
val updatedRoute = mapper.mapToDto(route)
|
||||
return if (created) {
|
||||
ResponseEntity.status(HttpStatus.CREATED).body(updatedRoute)
|
||||
} else {
|
||||
ResponseEntity.ok(updatedRoute)
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Delete route with id belonging to user",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "Route was deleted",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
), ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Route was not found",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@DeleteMapping("/{userId}/{id}")
|
||||
fun deleteRoute(
|
||||
@PathVariable @Parameter(description = "The id of the user",
|
||||
example = "1",
|
||||
required = true) userId: String,
|
||||
@PathVariable @Parameter(description = "The id of the route",
|
||||
example = "1",
|
||||
required = true) id: String,
|
||||
): ResponseEntity<Void> {
|
||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||
val routeIdConverted = RouteId.of(NonEmptyString(id))
|
||||
val userExists = userRepository.existsByUserId(userIdConverted)
|
||||
if (!userExists) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
}
|
||||
|
||||
val route = routeRepository.findByUserIdAndRouteId(userIdConverted, routeIdConverted)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
routeRepository.delete(route)
|
||||
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package de.twomartens.timetable.route
|
||||
|
||||
import de.twomartens.timetable.model.common.DepotId
|
||||
import de.twomartens.timetable.model.common.PortalId
|
||||
import de.twomartens.timetable.model.common.RouteId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.dto.Depot
|
||||
import de.twomartens.timetable.model.dto.Portal
|
||||
import de.twomartens.timetable.model.dto.TravelDuration
|
||||
import de.twomartens.timetable.model.dto.TswRoute
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import org.mapstruct.*
|
||||
|
||||
@Mapper(
|
||||
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
interface RouteMapper {
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "lastModified", ignore = true)
|
||||
fun mapToDB(userId: UserId, dto: TswRoute): de.twomartens.timetable.model.db.TswRoute {
|
||||
return de.twomartens.timetable.model.db.TswRoute(
|
||||
userId,
|
||||
RouteId.of(NonEmptyString(dto.id)),
|
||||
NonEmptyString(dto.name),
|
||||
dto.country,
|
||||
dto.stations,
|
||||
mapDepotsToDB(dto.depots),
|
||||
mapPortalsToDB(dto.portals)
|
||||
)
|
||||
}
|
||||
|
||||
fun mapDepotsToDB(depots: List<Depot>): List<de.twomartens.timetable.model.db.Depot> {
|
||||
return depots.map {
|
||||
de.twomartens.timetable.model.db.Depot(
|
||||
DepotId.of(NonEmptyString(it.id)),
|
||||
NonEmptyString(it.name),
|
||||
it.nearestStation,
|
||||
it.tracks,
|
||||
mapTravelDurationsToDB(it.travelDurations)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapPortalsToDB(portals: List<Portal>): List<de.twomartens.timetable.model.db.Portal> {
|
||||
return portals.map {
|
||||
de.twomartens.timetable.model.db.Portal(
|
||||
PortalId.of(NonEmptyString(it.id)),
|
||||
NonEmptyString(it.name),
|
||||
it.nearestStation,
|
||||
mapTravelDurationsToDB(it.travelDurations)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapTravelDurationsToDB(travelDurations: List<TravelDuration>): List<de.twomartens.timetable.model.db.TravelDuration> {
|
||||
return travelDurations.map {
|
||||
de.twomartens.timetable.model.db.TravelDuration(
|
||||
it.formation,
|
||||
it.time
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapRoutesToDto(routes: List<de.twomartens.timetable.model.db.TswRoute>): List<TswRoute>
|
||||
|
||||
fun mapToDto(db: de.twomartens.timetable.model.db.TswRoute): TswRoute {
|
||||
return TswRoute(
|
||||
db.routeId.id,
|
||||
db.name.value,
|
||||
db.country,
|
||||
db.stations,
|
||||
db.stations.first(),
|
||||
db.stations.last(),
|
||||
db.stations.size,
|
||||
mapDepotsToDto(db.depots),
|
||||
mapPortalsToDto(db.portals)
|
||||
)
|
||||
}
|
||||
|
||||
fun mapDepotsToDto(depots: List<de.twomartens.timetable.model.db.Depot>): List<Depot> {
|
||||
return depots.map { depot ->
|
||||
Depot(
|
||||
depot.id.id,
|
||||
depot.name.value,
|
||||
depot.nearestStation,
|
||||
depot.tracks,
|
||||
mapTravelDurationsToDto(depot.travelDurations)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapPortalsToDto(portals: List<de.twomartens.timetable.model.db.Portal>): List<Portal> {
|
||||
return portals.map { portal ->
|
||||
Portal(
|
||||
portal.id.id,
|
||||
portal.name.value,
|
||||
portal.nearestStation,
|
||||
mapTravelDurationsToDto(portal.travelDurations)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapTravelDurationsToDto(travelDurations: List<de.twomartens.timetable.model.db.TravelDuration>): List<TravelDuration> {
|
||||
return travelDurations.map {
|
||||
TravelDuration(
|
||||
it.formation,
|
||||
it.time
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
package de.twomartens.timetable.repository
|
||||
package de.twomartens.timetable.route
|
||||
|
||||
import de.twomartens.timetable.model.common.RouteId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.db.TswRoute
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
@@ -7,5 +8,8 @@ import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
|
||||
interface TswRouteRepository : MongoRepository<TswRoute, ObjectId> {
|
||||
fun findByUserIdAndRouteId(userId: UserId, routeId: RouteId): TswRoute?
|
||||
fun findByUserIdAndName(userId: UserId, name: NonEmptyString): TswRoute?
|
||||
fun findAllByUserId(userId: UserId): List<TswRoute>
|
||||
fun findAllByUserIdAndNameContainingIgnoreCase(userId: UserId, name: String): List<TswRoute>
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package de.twomartens.timetable.station
|
||||
|
||||
import de.twomartens.timetable.bahnApi.service.BahnApiService
|
||||
import de.twomartens.timetable.bahnApi.service.BahnDatabaseService
|
||||
import de.twomartens.timetable.model.dto.Station
|
||||
import de.twomartens.timetable.model.mapper.StationMapper
|
||||
import de.twomartens.timetable.model.repository.StationRepository
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.mapstruct.factory.Mappers
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/v1/stations"])
|
||||
@Tag(name = "Stations", description = "all requests relating to stations")
|
||||
class StationController(
|
||||
private val stationRepository: StationRepository,
|
||||
private val bahnApiService: BahnApiService,
|
||||
private val bahnDatabaseService: BahnDatabaseService
|
||||
) {
|
||||
private val mapper = Mappers.getMapper(StationMapper::class.java)
|
||||
|
||||
@Operation(
|
||||
summary = "Access stations and filter by name",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "List of found stations",
|
||||
content = [Content(
|
||||
array = ArraySchema(schema = Schema(implementation = Station::class))
|
||||
)]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@GetMapping("/")
|
||||
fun getStations(
|
||||
@RequestParam(name = "country", required = false) @Parameter(description = "Searched country",
|
||||
example = "de",
|
||||
required = false) countryCode: String?,
|
||||
@RequestParam(name = "name", required = false) @Parameter(description = "Searched name",
|
||||
example = "Aachen",
|
||||
required = false) name: String?
|
||||
): ResponseEntity<List<Station>> {
|
||||
val stations = findStations(countryCode, name)
|
||||
|
||||
return ResponseEntity.ok(mapper.mapStationsToDto(stations))
|
||||
}
|
||||
|
||||
private fun findStations(
|
||||
countryCode: String?,
|
||||
name: String?
|
||||
): List<de.twomartens.timetable.model.db.Station> {
|
||||
val stations = if (!name.isNullOrBlank()) {
|
||||
if (!countryCode.isNullOrBlank()) {
|
||||
stationRepository.findAllByCountryCodeAndNameContainingIgnoreCase(countryCode, name)
|
||||
} else {
|
||||
stationRepository.findAllByNameContainingIgnoreCase(name)
|
||||
}
|
||||
} else {
|
||||
stationRepository.findAll()
|
||||
}
|
||||
return stations
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Update stations",
|
||||
responses = [ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Stations updated",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
), ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "Access forbidden for user",
|
||||
content = [Content(mediaType = "text/plain")]
|
||||
)]
|
||||
)
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
@PostMapping("/update")
|
||||
fun updateStations(): ResponseEntity<Void> {
|
||||
val stations = bahnApiService.fetchStations("*")
|
||||
bahnDatabaseService.storeStations(stations)
|
||||
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.property.TimeProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.time.Clock
|
||||
|
||||
@Configuration
|
||||
open class ClockConfiguration(private val properties: TimeProperties) {
|
||||
|
||||
@Bean
|
||||
open fun clock(): Clock {
|
||||
return Clock.system(properties.defaultTimeZone)
|
||||
}
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter
|
||||
|
||||
@Configuration
|
||||
open class FilterConfiguration {
|
||||
@Bean
|
||||
open fun forwardedFilter(): ForwardedHeaderFilter {
|
||||
return ForwardedHeaderFilter()
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.timetable.support.interceptor.LoggingInterceptorRest
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.time.Clock
|
||||
|
||||
@Configuration
|
||||
open class InterceptorConfiguration {
|
||||
@Bean
|
||||
open fun loggingInterceptorRest(clock: Clock): LoggingInterceptorRest {
|
||||
return LoggingInterceptorRest(clock)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun headerInterceptorRest(): HeaderInterceptorRest {
|
||||
return HeaderInterceptorRest()
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.property.RestTemplateTimeoutProperties
|
||||
import de.twomartens.timetable.support.interceptor.HeaderInterceptorRest
|
||||
import de.twomartens.timetable.support.interceptor.LoggingInterceptorRest
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.client.RestTemplate
|
||||
|
||||
@Configuration
|
||||
open class RestTemplateConfiguration {
|
||||
@Bean("restTemplate")
|
||||
open fun restTemplate(
|
||||
headerInterceptorRest: HeaderInterceptorRest,
|
||||
loggingInterceptor: LoggingInterceptorRest,
|
||||
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
|
||||
): RestTemplate {
|
||||
return RestTemplateBuilder()
|
||||
.additionalInterceptors(headerInterceptorRest, loggingInterceptor)
|
||||
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestTemplateTimeoutInMillis)
|
||||
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestTemplateInMillis)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Bean("restTemplateRestHealthIndicator")
|
||||
open fun restTemplateRestHealthIndicator(
|
||||
headerInterceptorRest: HeaderInterceptorRest,
|
||||
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
|
||||
): RestTemplate {
|
||||
return RestTemplateBuilder()
|
||||
.additionalInterceptors(headerInterceptorRest)
|
||||
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestHealthIndicatorTimeoutInMillis)
|
||||
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestHealthIndicatorInMillis)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package de.twomartens.timetable.support.configuration
|
||||
|
||||
import de.twomartens.timetable.support.monitoring.statusprobe.CountBasedStatusProbe
|
||||
import de.twomartens.timetable.support.monitoring.statusprobe.StatusProbe
|
||||
import de.twomartens.timetable.support.monitoring.statusprobe.StatusProbeCriticality
|
||||
import de.twomartens.timetable.support.monitoring.statusprobe.StatusProbeLogger
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.time.Clock
|
||||
|
||||
@Configuration
|
||||
open class StatusProbeConfiguration(private val clock: Clock) {
|
||||
@Bean
|
||||
open fun statusProbeLogger(): StatusProbeLogger {
|
||||
return StatusProbeLogger(clock)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun testStatusProbe(statusProbeLogger: StatusProbeLogger): StatusProbe {
|
||||
return CountBasedStatusProbe(
|
||||
1,
|
||||
clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger
|
||||
)
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package de.twomartens.timetable.support.controller
|
||||
|
||||
import de.twomartens.timetable.support.exception.HttpStatusException
|
||||
import de.twomartens.timetable.support.model.dto.ErrorMessage
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
|
||||
|
||||
@ControllerAdvice(annotations = [RestController::class])
|
||||
class ExceptionController : ResponseEntityExceptionHandler() {
|
||||
@ExceptionHandler(HttpStatusException::class)
|
||||
fun handleException(e: HttpStatusException): ResponseEntity<ErrorMessage> {
|
||||
if (e.cause != null) {
|
||||
log.info(e.cause.toString(), e.cause)
|
||||
} else {
|
||||
log.info(e.toString())
|
||||
}
|
||||
return ResponseEntity.status(e.status)
|
||||
.body(ErrorMessage(e.message!!))
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException::class)
|
||||
fun handleRuntimeException(e: RuntimeException): ResponseEntity<ErrorMessage> {
|
||||
log.error("unexpected exception occurred", e)
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ErrorMessage(message = e.message!!))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
package de.twomartens.timetable.support.controller
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.ModelAttribute
|
||||
import org.springframework.web.bind.annotation.RequestHeader
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Paths
|
||||
import java.util.jar.JarFile
|
||||
|
||||
@Controller
|
||||
@RequestMapping(value = ["/timetable"])
|
||||
class VersionHtmlController {
|
||||
@GetMapping(path = ["/version"])
|
||||
fun version(): String {
|
||||
return "version"
|
||||
}
|
||||
|
||||
@ModelAttribute("version")
|
||||
private fun getApplicationVersion(): String {
|
||||
return "${getTitle()} ${getVersion()}"
|
||||
}
|
||||
|
||||
@ModelAttribute("footerString")
|
||||
private fun getApplicationVersion(@RequestHeader("host") hostName: String): String {
|
||||
return "${getTitle()} ${getVersion()} - $hostName"
|
||||
}
|
||||
|
||||
private fun getTitle(): String {
|
||||
val title = VersionHtmlController::class.java.`package`.implementationTitle
|
||||
?: return "application"
|
||||
return title.ifBlank {
|
||||
"application"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVersion(): String {
|
||||
val version = VersionHtmlController::class.java.`package`.implementationVersion
|
||||
?: return "DEVELOPER"
|
||||
return version.ifBlank {
|
||||
"DEVELOPER"
|
||||
}
|
||||
}
|
||||
|
||||
@ModelAttribute("hostname")
|
||||
private fun getHostname(): String {
|
||||
try {
|
||||
return InetAddress.getLocalHost().hostName
|
||||
} catch (e: UnknownHostException) {
|
||||
log.warn(e.toString(), e)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@ModelAttribute("manifest")
|
||||
private fun getManifest(): Collection<String> {
|
||||
try {
|
||||
val location = javaClass.getProtectionDomain().codeSource.location
|
||||
val jarFileName = Paths.get(location.toURI()).toString()
|
||||
JarFile(jarFileName).use { jarFile ->
|
||||
val entry = jarFile.getEntry(JarFile.MANIFEST_NAME)
|
||||
jarFile.getInputStream(entry).use { `in` ->
|
||||
return String(`in`.readAllBytes(), StandardCharsets.UTF_8).lines().toList()
|
||||
}
|
||||
}
|
||||
} catch (ignored: FileNotFoundException) {
|
||||
// do nothing if manifest file is not available
|
||||
} catch (e: Exception) {
|
||||
log.info(e.toString(), e)
|
||||
}
|
||||
return listOf("${getTitle()} ${getVersion()}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package de.twomartens.timetable.support.controller.v1
|
||||
|
||||
import de.twomartens.timetable.property.ServiceProperties
|
||||
import io.swagger.v3.oas.annotations.Hidden
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/timetable/v1"])
|
||||
class HealthCheckController(private val properties: ServiceProperties) {
|
||||
|
||||
|
||||
@Hidden
|
||||
@GetMapping("/healthCheck")
|
||||
fun healthCheck(message: String): String {
|
||||
return properties.greeting.format(message)
|
||||
}
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
package de.twomartens.timetable.support.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
class HttpStatusException(val status: HttpStatus, message: String, cause: Throwable?) :
|
||||
RuntimeException(message, cause)
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package de.twomartens.timetable.support.interceptor
|
||||
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatusCode
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.util.StreamUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class BufferingClientHttpResponseWrapper(private val response: ClientHttpResponse) : ClientHttpResponse {
|
||||
private var body: ByteArray? = null
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getStatusCode(): HttpStatusCode {
|
||||
return response.statusCode
|
||||
}
|
||||
|
||||
@Deprecated("Replace calls of getRawStatusCode with getStatusCode", ReplaceWith("getStatusCode()"))
|
||||
@Throws(IOException::class)
|
||||
override fun getRawStatusCode(): Int {
|
||||
return statusCode.value()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getStatusText(): String {
|
||||
return response.statusText
|
||||
}
|
||||
|
||||
override fun getHeaders(): HttpHeaders {
|
||||
return response.headers
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getBody(): InputStream {
|
||||
if (body == null) {
|
||||
body = StreamUtils.copyToByteArray(response.body)
|
||||
}
|
||||
return ByteArrayInputStream(body)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
package de.twomartens.timetable.support.interceptor
|
||||
|
||||
import org.slf4j.MDC
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
|
||||
abstract class HeaderInterceptor {
|
||||
companion object {
|
||||
const val LOGGER_TRACE_ID = "trace.id"
|
||||
const val LOGGER_REQTYPE_ID = "REQTYPE"
|
||||
|
||||
const val HEADER_FIELD_TRACE_ID = "X-TraceId"
|
||||
const val HEADER_FIELD_B3_TRACE_ID = "x-b3-traceid"
|
||||
const val HEADER_FIELD_TYPE_ID = "x-type"
|
||||
|
||||
const val REQ_TYPE_HEALTHCHECK = "HEALTH_CHECK"
|
||||
const val REQ_TYPE_INTEGRATION_TEST = "INTEGRATION_TEST"
|
||||
const val REQ_TYPE_SERVER_TEST = "SERVER_TEST"
|
||||
const val REQ_TYPE_WARMUP = "WARMUP"
|
||||
|
||||
fun createNewTraceId(): String {
|
||||
return UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
fun getTraceId(): String {
|
||||
val traceId = MDC.get(LOGGER_TRACE_ID)
|
||||
return if (traceId.isNullOrBlank()) createNewTraceId() else traceId
|
||||
}
|
||||
|
||||
fun getRequestType(): String? {
|
||||
val type = MDC.get(LOGGER_REQTYPE_ID)
|
||||
return if (type.isNullOrBlank()) null else type
|
||||
}
|
||||
|
||||
private fun setTraceId(traceId: String): InterceptorCloseables {
|
||||
return InterceptorCloseables(MDC.putCloseable(LOGGER_TRACE_ID, traceId))
|
||||
}
|
||||
|
||||
private fun mark(requestType: String?): InterceptorCloseables {
|
||||
return InterceptorCloseables(MDC.putCloseable(LOGGER_REQTYPE_ID, requestType))
|
||||
}
|
||||
|
||||
fun set(traceId: String, requestType: String?): InterceptorCloseables {
|
||||
return if (requestType != null) {
|
||||
InterceptorCloseables(setTraceId(traceId), mark(requestType))
|
||||
} else setTraceId(traceId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun markAsHealthCheck(): InterceptorCloseables {
|
||||
return InterceptorCloseables(mark(REQ_TYPE_HEALTHCHECK), setTraceId(createNewTraceId()))
|
||||
}
|
||||
|
||||
fun markAsIntegrationTest(): InterceptorCloseables {
|
||||
return InterceptorCloseables(mark(REQ_TYPE_INTEGRATION_TEST), setTraceId(createNewTraceId()))
|
||||
}
|
||||
|
||||
fun markAsServerTest(): InterceptorCloseables {
|
||||
return InterceptorCloseables(mark(REQ_TYPE_SERVER_TEST), setTraceId(createNewTraceId()))
|
||||
}
|
||||
|
||||
fun markAsWarmup(): InterceptorCloseables {
|
||||
return InterceptorCloseables(mark(REQ_TYPE_WARMUP), setTraceId(createNewTraceId()))
|
||||
}
|
||||
|
||||
class InterceptorCloseables(vararg closeables: Closeable) : Closeable {
|
||||
private val closeables: Array<out Closeable>
|
||||
|
||||
init {
|
||||
this.closeables = closeables
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
closeables.forEach {
|
||||
try {
|
||||
it.close()
|
||||
} catch (ignored: Exception) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
package de.twomartens.timetable.support.interceptor
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.http.HttpRequest
|
||||
import org.springframework.http.client.ClientHttpRequestExecution
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class HeaderInterceptorRest : HeaderInterceptor(), HandlerInterceptor, ClientHttpRequestInterceptor {
|
||||
companion object {
|
||||
val CLASS_NAME: String = HeaderInterceptorRest::class.java.getName()
|
||||
|
||||
fun extractTraceId(request: HttpServletRequest): String {
|
||||
var traceId = request.getHeader(HEADER_FIELD_TRACE_ID)
|
||||
if (traceId.isNullOrBlank()) traceId = request.getHeader(HEADER_FIELD_B3_TRACE_ID)
|
||||
if (traceId.isNullOrBlank()) return createNewTraceId()
|
||||
return traceId
|
||||
}
|
||||
|
||||
fun extractTraceId(request: HttpRequest): String {
|
||||
var traceId = request.headers[HEADER_FIELD_TRACE_ID]?.first()
|
||||
if (traceId.isNullOrBlank()) traceId = request.headers[HEADER_FIELD_B3_TRACE_ID]?.first()
|
||||
if (traceId.isNullOrBlank()) return UUID.randomUUID().toString()
|
||||
return traceId
|
||||
}
|
||||
|
||||
fun extractRequestType(request: HttpServletRequest): String? {
|
||||
val type = request.getHeader(HEADER_FIELD_TYPE_ID)
|
||||
if (type.isNullOrBlank()) return null
|
||||
return type
|
||||
}
|
||||
|
||||
fun extractRequestType(request: HttpRequest): String? {
|
||||
val type = request.headers[HEADER_FIELD_TYPE_ID]?.first()
|
||||
if (type.isNullOrBlank()) return null
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
// ClientHttpRequestInterceptor
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(
|
||||
request: HttpRequest, body: ByteArray,
|
||||
execution: ClientHttpRequestExecution
|
||||
): ClientHttpResponse {
|
||||
request.headers.add(HEADER_FIELD_TRACE_ID, getTraceId())
|
||||
val requestType = getRequestType()
|
||||
if (requestType != null) {
|
||||
request.headers.add(HEADER_FIELD_TYPE_ID, requestType)
|
||||
}
|
||||
|
||||
return try {
|
||||
execution.execute(request, body)
|
||||
} finally {
|
||||
request.headers.remove(HEADER_FIELD_TRACE_ID)
|
||||
request.headers.remove(HEADER_FIELD_TYPE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
// HandlerInterceptor
|
||||
override fun preHandle(
|
||||
request: HttpServletRequest, response: HttpServletResponse,
|
||||
handler: Any
|
||||
): Boolean {
|
||||
val traceId = extractTraceId(request)
|
||||
val requestType = extractRequestType(request)
|
||||
val closeable = set(traceId, requestType)
|
||||
request.setAttribute(CLASS_NAME, closeable)
|
||||
return true
|
||||
}
|
||||
|
||||
// HandlerInterceptor
|
||||
// override fun postHandle(
|
||||
// request: HttpServletRequest,
|
||||
// response: HttpServletResponse,
|
||||
// handler: Any,
|
||||
// modelAndView: ModelAndView?
|
||||
// ) {
|
||||
// val obj = request.getAttribute(CLASS_NAME)
|
||||
// if (obj != null && obj is InterceptorCloseables) {
|
||||
// obj.close()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
-438
@@ -1,438 +0,0 @@
|
||||
package de.twomartens.timetable.support.interceptor
|
||||
|
||||
import jakarta.servlet.*
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import mu.KotlinLogging
|
||||
import org.apache.logging.log4j.message.StringMapMessage
|
||||
import org.slf4j.Marker
|
||||
import org.slf4j.MarkerFactory
|
||||
import org.springframework.http.*
|
||||
import org.springframework.http.client.ClientHttpRequestExecution
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.util.StreamUtils
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import java.util.function.Function
|
||||
|
||||
class LoggingInterceptorRest(
|
||||
private val requestLogBehaviour: FieldLogBehaviour,
|
||||
private val responseLogBehaviour: FieldLogBehaviour,
|
||||
val clock: Clock
|
||||
) : Filter, ClientHttpRequestInterceptor {
|
||||
|
||||
constructor(clock: Clock) : this(FieldLogBehaviour.NEVER, FieldLogBehaviour.NEVER, clock)
|
||||
|
||||
@Throws(IOException::class, ServletException::class)
|
||||
override fun doFilter(
|
||||
request: ServletRequest, response: ServletResponse,
|
||||
chain: FilterChain
|
||||
) {
|
||||
val requestTime = ZonedDateTime.now(clock)
|
||||
val httpRequest = request as HttpServletRequest
|
||||
val requestWrapper = ContentCachingRequestWrapper(httpRequest)
|
||||
val responseWrapper = ContentCachingResponseWrapper(
|
||||
(response as HttpServletResponse)
|
||||
)
|
||||
var fullResponseBytes: ByteArray? = null
|
||||
var throwable: Throwable? = null
|
||||
var responseBody: String? = null
|
||||
var httpStatusCode = -1
|
||||
try {
|
||||
try {
|
||||
chain.doFilter(
|
||||
if (requestLogBehaviour != FieldLogBehaviour.NEVER) requestWrapper else httpRequest,
|
||||
responseWrapper
|
||||
)
|
||||
if (responseLogBehaviour != FieldLogBehaviour.NEVER) {
|
||||
fullResponseBytes = responseWrapper.contentAsByteArray
|
||||
}
|
||||
httpStatusCode = responseWrapper.status
|
||||
} finally {
|
||||
responseWrapper.copyBodyToResponse()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throwable = e
|
||||
throw e
|
||||
} finally {
|
||||
try {
|
||||
val responseSize = responseWrapper.contentSize
|
||||
val responseHeaders = extractHeaders(
|
||||
headerNames = responseWrapper.headerNames.iterator()
|
||||
) { responseWrapper.getHeaders(it).iterator() }
|
||||
if (
|
||||
(responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS)
|
||||
&& fullResponseBytes != null
|
||||
) {
|
||||
responseBody = String(
|
||||
fullResponseBytes,
|
||||
determineResponseEncoding()
|
||||
)
|
||||
}
|
||||
val query = if (httpRequest.queryString != null) "?${httpRequest.queryString}" else ""
|
||||
val requestUrl = URL("${httpRequest.requestURL}$query")
|
||||
val requestHeaders = extractHeaders(
|
||||
headerNames = httpRequest.headerNames.asIterator(),
|
||||
ignoreList = listOf("authorization")) {
|
||||
httpRequest.getHeaders(it).asIterator()
|
||||
}
|
||||
var requestBody: String? = null
|
||||
var businessType: String? = null
|
||||
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||
) {
|
||||
val fullRequestBytes = requestWrapper.contentAsByteArray
|
||||
requestBody = String(fullRequestBytes, determineRequestEncoding())
|
||||
businessType = determineBusinessType()
|
||||
}
|
||||
log(
|
||||
LogMessage(
|
||||
requestHeaders = requestHeaders,
|
||||
responseHeaders = responseHeaders,
|
||||
url = requestUrl,
|
||||
method = httpRequest.method,
|
||||
requestMimeType = typeToString(request.getContentType()),
|
||||
responseMimeType = typeToString(response.getContentType()),
|
||||
requestBody = requestBody,
|
||||
responseBody = responseBody,
|
||||
requestSize = httpRequest.contentLength,
|
||||
responseSize = responseSize,
|
||||
httpStatus = httpStatusCode,
|
||||
direction = DIRECTION_IN,
|
||||
requestTime = requestTime,
|
||||
responseTime = ZonedDateTime.now(clock),
|
||||
traceId = HeaderInterceptor.getTraceId(),
|
||||
requestType = HeaderInterceptor.getRequestType(),
|
||||
businessType = businessType,
|
||||
throwable = throwable
|
||||
)
|
||||
)
|
||||
val interceptorCloseables =
|
||||
request.getAttribute(HeaderInterceptorRest.CLASS_NAME) as HeaderInterceptor.InterceptorCloseables
|
||||
interceptorCloseables.close()
|
||||
} catch (e: java.lang.RuntimeException) {
|
||||
log.error(e.toString(), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isError(httpStatusCode: Int): Boolean {
|
||||
return httpStatusCode in 400..599
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(
|
||||
request: HttpRequest,
|
||||
requestBytes: ByteArray, execution: ClientHttpRequestExecution
|
||||
): ClientHttpResponse {
|
||||
val requestTime = ZonedDateTime.now(clock)
|
||||
var responseSize = 0
|
||||
var responseHeaders: Map<String, Collection<String>> = emptyMap()
|
||||
var responseMediaType: MediaType? = null
|
||||
var httpStatusCode = -1
|
||||
var throwable: Throwable? = null
|
||||
var requestBody: String? = null
|
||||
var responseBody: String? = null
|
||||
var businessType: String? = null
|
||||
return try {
|
||||
val result = BufferingClientHttpResponseWrapper(
|
||||
execution.execute(request, requestBytes)
|
||||
)
|
||||
val responseBytes = StreamUtils.copyToByteArray(result.getBody())
|
||||
responseSize = responseBytes.size
|
||||
responseHeaders = extractHeaders(result.headers)
|
||||
responseMediaType = result.headers.getContentType()
|
||||
httpStatusCode = result.statusCode.value()
|
||||
if (responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||
) {
|
||||
responseBody = String(
|
||||
responseBytes,
|
||||
determineRequestEncoding()
|
||||
)
|
||||
}
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
throwable = e
|
||||
throw e
|
||||
} finally {
|
||||
try {
|
||||
val url = request.uri.toURL()
|
||||
val requestHeaders: Map<String, Collection<String>> = extractHeaders(request.headers)
|
||||
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||
) {
|
||||
requestBody = String(
|
||||
requestBytes,
|
||||
determineRequestEncoding()
|
||||
)
|
||||
businessType = determineBusinessType()
|
||||
}
|
||||
log(
|
||||
LogMessage(
|
||||
requestHeaders = requestHeaders,
|
||||
responseHeaders = responseHeaders,
|
||||
url = url,
|
||||
method = request.method.name(),
|
||||
requestMimeType = typeToString(request.headers.getContentType()),
|
||||
requestBody = requestBody,
|
||||
responseBody = responseBody,
|
||||
responseMimeType = typeToString(responseMediaType!!),
|
||||
requestSize = requestBytes.size,
|
||||
responseSize = responseSize,
|
||||
httpStatus = httpStatusCode,
|
||||
direction = DIRECTION_OUT,
|
||||
requestTime = requestTime,
|
||||
responseTime = ZonedDateTime.now(clock),
|
||||
businessType = businessType,
|
||||
throwable = throwable,
|
||||
traceId = HeaderInterceptorRest.extractTraceId(request),
|
||||
requestType = HeaderInterceptorRest.extractRequestType(request),
|
||||
)
|
||||
)
|
||||
} catch (e: java.lang.RuntimeException) {
|
||||
log.error(e.toString(), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun log(logMessage: LogMessage) {
|
||||
val stringMapMessage = StringMapMessage()
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_REQUEST_HEADERS,
|
||||
toHeaderString(logMessage.requestHeaders)
|
||||
)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_RESPONSE_HEADERS,
|
||||
toHeaderString(logMessage.responseHeaders)
|
||||
)
|
||||
addLogString(stringMapMessage, PARAM_URL_FULL, logMessage.url.toString())
|
||||
addLogString(stringMapMessage, PARAM_URL_DOMAIN, logMessage.url.host)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_URL_EXTENSION,
|
||||
extractExtension(logMessage.url.path)
|
||||
)
|
||||
addLogString(stringMapMessage, PARAM_URL_PATH, logMessage.url.path)
|
||||
addLogString(stringMapMessage, PARAM_URL_PORT, logMessage.url.port.toString())
|
||||
addLogString(stringMapMessage, PARAM_URL_SCHEME, logMessage.url.protocol)
|
||||
addLogString(stringMapMessage, PARAM_URL_QUERY, logMessage.url.query)
|
||||
addLogString(stringMapMessage, PARAM_REQUEST_METHOD, logMessage.method)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_REQUEST_REFERER,
|
||||
getHeader(logMessage.requestHeaders, HttpHeaders.REFERER)
|
||||
)
|
||||
addLogString(stringMapMessage, PARAM_REQUEST_MIMETYPE, logMessage.requestMimeType)
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_MIMETYPE, logMessage.responseMimeType)
|
||||
addLogString(stringMapMessage, PARAM_REQUEST_BYTES, logMessage.requestSize.toString())
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_BYTES, logMessage.responseSize.toString())
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_STATUS, logMessage.httpStatus.toString())
|
||||
addLogString(stringMapMessage, PARAM_DIRECTION, logMessage.direction)
|
||||
addLogString(stringMapMessage, PARAM_PROTOCOL, PROTOCOL_NAME)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_REQUEST_TIME,
|
||||
logMessage.requestTime.format(DATE_TIME_FORMATTER)
|
||||
)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_RESPONSE_TIME,
|
||||
logMessage.responseTime.format(DATE_TIME_FORMATTER)
|
||||
)
|
||||
addLogString(
|
||||
stringMapMessage,
|
||||
PARAM_DURATION,
|
||||
getDurationBetweenRequestAndResponseTime(logMessage).toNanos().toString()
|
||||
)
|
||||
addLogString(
|
||||
stringMapMessage, PARAM_USER_AGENT,
|
||||
getHeader(logMessage.requestHeaders, HttpHeaders.USER_AGENT)
|
||||
)
|
||||
addLogString(stringMapMessage, HeaderInterceptor.LOGGER_TRACE_ID, logMessage.traceId)
|
||||
addLogString(stringMapMessage, HeaderInterceptor.LOGGER_REQTYPE_ID, logMessage.requestType)
|
||||
addLogString(stringMapMessage, PARAM_BUSINESS_TYPE, logMessage.businessType)
|
||||
addLogString(stringMapMessage, PARAM_REQUEST_BODY, cutToMaxLength(logMessage.requestBody))
|
||||
addLogString(stringMapMessage, PARAM_RESPONSE_BODY, cutToMaxLength(logMessage.responseBody))
|
||||
log.debug(MARKER, stringMapMessage.asString(), logMessage.throwable)
|
||||
}
|
||||
|
||||
private fun getDurationBetweenRequestAndResponseTime(logMessage: LogMessage): Duration {
|
||||
return Duration.between(logMessage.requestTime, logMessage.responseTime)
|
||||
}
|
||||
|
||||
private fun getHeader(headers: Map<String, Collection<String?>>, headerKey: String): String? {
|
||||
return headers.entries.asSequence()
|
||||
.filter { it.key.equals(headerKey, ignoreCase = true) }
|
||||
.flatMap { it.value.asSequence() }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun addLogString(stringMapMessage: StringMapMessage, key: String, value: String?) {
|
||||
if (!value.isNullOrBlank()) {
|
||||
stringMapMessage.with(key, value.trim { it <= ' ' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns null, but can be overridden to implement more complex logic
|
||||
*/
|
||||
private fun determineBusinessType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns UTF-8, but can be overridden to implement more complex logic
|
||||
*/
|
||||
private fun determineRequestEncoding(): Charset {
|
||||
return StandardCharsets.UTF_8
|
||||
}
|
||||
|
||||
/**
|
||||
* usually returns UTF-8, but can be overridden to implement more complex logic
|
||||
*/
|
||||
private fun determineResponseEncoding(): Charset {
|
||||
return StandardCharsets.UTF_8
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
private const val MAX_LOG_SIZE = 20480 // 20 KB - logging could fail with bigger logmessages
|
||||
|
||||
val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
|
||||
val MARKER: Marker = MarkerFactory.getMarker("communication")
|
||||
|
||||
const val DIRECTION_IN = "inbound"
|
||||
const val DIRECTION_OUT = "outbound"
|
||||
const val PROTOCOL_NAME = "http"
|
||||
|
||||
const val PARAM_URL_FULL = "url.full"
|
||||
const val PARAM_URL_DOMAIN = "url.domain"
|
||||
const val PARAM_URL_EXTENSION = "url.extension"
|
||||
const val PARAM_URL_PATH = "url.path"
|
||||
const val PARAM_URL_PORT = "url.port"
|
||||
const val PARAM_URL_SCHEME = "url.scheme"
|
||||
const val PARAM_URL_QUERY = "url.query"
|
||||
const val PARAM_BUSINESS_TYPE = "http.request.type"
|
||||
const val PARAM_REQUEST_BODY = "http.request.body.content"
|
||||
const val PARAM_RESPONSE_BODY = "http.response.body.content"
|
||||
const val PARAM_RESPONSE_STATUS = "http.response.status_code"
|
||||
const val PARAM_REQUEST_HEADERS = "http.request.headers"
|
||||
const val PARAM_RESPONSE_HEADERS = "http.response.headers"
|
||||
const val PARAM_REQUEST_BYTES = "http.request.body.bytes"
|
||||
const val PARAM_RESPONSE_BYTES = "http.response.body.bytes"
|
||||
const val PARAM_REQUEST_MIMETYPE = "http.request.mime_type"
|
||||
const val PARAM_RESPONSE_MIMETYPE = "http.response.mime_type"
|
||||
const val PARAM_REQUEST_METHOD = "http.request.method"
|
||||
const val PARAM_REQUEST_REFERER = "http.request.referrer"
|
||||
const val PARAM_REQUEST_TIME = "event.start"
|
||||
const val PARAM_RESPONSE_TIME = "event.end"
|
||||
const val PARAM_DURATION = "event.duration"
|
||||
const val PARAM_USER_AGENT = "user_agent.original"
|
||||
const val PARAM_DIRECTION = "network.direction"
|
||||
const val PARAM_PROTOCOL = "network.protocol"
|
||||
|
||||
private fun extractHeaders(
|
||||
headerNames: Iterator<String>,
|
||||
ignoreList: List<String> = emptyList(),
|
||||
headerValuesSupplier: Function<String, Iterator<String>>
|
||||
): Map<String, MutableCollection<String>> {
|
||||
val requestHeaders: MutableMap<String, MutableCollection<String>> = mutableMapOf()
|
||||
while (headerNames.hasNext()) {
|
||||
val name = headerNames.next()
|
||||
if (name in ignoreList) continue
|
||||
val values = requestHeaders.computeIfAbsent(name) { mutableSetOf() }
|
||||
val headerValues = headerValuesSupplier.apply(name)
|
||||
while (headerValues.hasNext()) {
|
||||
values.add(headerValues.next())
|
||||
}
|
||||
}
|
||||
return requestHeaders
|
||||
}
|
||||
|
||||
private fun extractHeaders(headers: HttpHeaders): Map<String, Collection<String>> {
|
||||
val result: MutableMap<String, Collection<String>> = mutableMapOf()
|
||||
for ((key, value) in headers) {
|
||||
result[key] = value.toList()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun toHeaderString(headerMap: Map<String, Collection<String>>): String {
|
||||
return headerMap.entries.asSequence()
|
||||
.flatMap { entry ->
|
||||
val key = entry.key
|
||||
entry.value.asSequence().map { Pair(key, it) }
|
||||
}
|
||||
.map { "${it.first}=${it.second}" }
|
||||
.joinToString(separator = ",")
|
||||
}
|
||||
|
||||
private fun typeToString(contentType: String?): String? {
|
||||
return try {
|
||||
if (contentType == null) return null
|
||||
val mediaType = MediaType.parseMediaType(contentType)
|
||||
return typeToString(mediaType)
|
||||
} catch (e: InvalidMediaTypeException) {
|
||||
log.info(e.toString(), e)
|
||||
e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun typeToString(mediaType: MediaType?): String? {
|
||||
return try {
|
||||
if (mediaType == null) return null
|
||||
"${mediaType.type}/${mediaType.subtype}"
|
||||
} catch (e: RuntimeException) {
|
||||
log.info(e.toString(), e)
|
||||
e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractExtension(fileName: String?): String? {
|
||||
if (fileName == null || !fileName.contains(".")) return null
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1)
|
||||
}
|
||||
|
||||
private fun cutToMaxLength(string: String?): String? {
|
||||
return if (string != null && string.length > MAX_LOG_SIZE) {
|
||||
string.substring(0, MAX_LOG_SIZE)
|
||||
} else string
|
||||
}
|
||||
|
||||
private data class LogMessage(
|
||||
val requestHeaders: Map<String, Collection<String>>,
|
||||
val responseHeaders: Map<String, Collection<String>>,
|
||||
val url: URL,
|
||||
val method: String,
|
||||
val requestMimeType: String?,
|
||||
val responseMimeType: String?,
|
||||
val requestBody: String?,
|
||||
val responseBody: String?,
|
||||
val requestSize: Int,
|
||||
val responseSize: Int,
|
||||
val httpStatus: Int,
|
||||
val direction: String,
|
||||
val requestTime: ZonedDateTime,
|
||||
val responseTime: ZonedDateTime,
|
||||
val traceId: String?,
|
||||
val requestType: String?,
|
||||
val businessType: String?,
|
||||
val throwable: Throwable?
|
||||
)
|
||||
|
||||
enum class FieldLogBehaviour {
|
||||
NEVER,
|
||||
ONLY_ON_ERROR,
|
||||
ALWAYS
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
package de.twomartens.timetable.support.model
|
||||
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class LeadershipStatus {
|
||||
var isLeader = false
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
package de.twomartens.timetable.support.model.dto
|
||||
|
||||
data class ErrorMessage(val message: String)
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
package de.twomartens.timetable.support.monitoring.actuator
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.boot.actuate.health.Health
|
||||
import org.springframework.boot.actuate.health.HealthIndicator
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import java.io.IOException
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
abstract class AbstractHealthIndicator(
|
||||
private val clock: Clock,
|
||||
private val preparable: Preparable
|
||||
) : HealthIndicator {
|
||||
|
||||
private val logStatusDownMessage = "health indicator '${indicatorName()}' invoked with status '${Status.DOWN.code}'"
|
||||
private val logStatusUpMessage = "health indicator '${indicatorName()}' invoked with status '${Status.UP.code}'"
|
||||
|
||||
private var firstTime = true
|
||||
|
||||
/**
|
||||
* main method that determines the health of the service
|
||||
*/
|
||||
protected abstract fun determineHealth(): Health
|
||||
|
||||
override fun health(): Health {
|
||||
try {
|
||||
preparable.prepare().use {
|
||||
var result: Health? = null
|
||||
var exception: Exception? = null
|
||||
val start = clock.millis()
|
||||
try {
|
||||
result = determineHealth()
|
||||
} catch (e: RuntimeException) {
|
||||
exception = e
|
||||
result = Health.down().withException(e).build()
|
||||
} finally {
|
||||
logInvocation(result, exception, start, clock.millis())
|
||||
}
|
||||
return result!!
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error("unexpected exception occurred", e)
|
||||
return Health.down(e).build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun logInvocation(health: Health?, exception: Exception?, start: Long, end: Long) {
|
||||
val duration = Duration.ofMillis(end - start)
|
||||
MDC.putCloseable("event.duration", duration.toNanos().toString()).use {
|
||||
if (exception != null || health == null) {
|
||||
log.error(logStatusDownMessage, exception)
|
||||
firstTime = true
|
||||
} else if (health.status === Status.DOWN) {
|
||||
log.warn(logStatusDownMessage)
|
||||
firstTime = true
|
||||
} else if (firstTime) {
|
||||
log.info(logStatusUpMessage)
|
||||
firstTime = false
|
||||
} else {
|
||||
log.trace(logStatusUpMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun indicatorName(): String {
|
||||
return this.javaClass.getSimpleName()
|
||||
.replace("HealthIndicator", "")
|
||||
.lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val HOST = "localhost"
|
||||
const val HTTP_PREFIX = "http://"
|
||||
const val HOST_PORT_SEPARATOR = ":"
|
||||
const val PATH_SEPARATOR = "/"
|
||||
const val PARAMETER_SEPARATOR = "?"
|
||||
const val DETAIL_ENDPOINT_KEY = "endpoint"
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
package de.twomartens.timetable.support.monitoring.actuator
|
||||
|
||||
import de.twomartens.timetable.support.monitoring.statusprobe.StatusProbe
|
||||
import org.springframework.boot.actuate.health.Health
|
||||
import org.springframework.boot.actuate.health.HealthIndicator
|
||||
import java.time.Clock
|
||||
|
||||
abstract class AbstractStatusProbeHealthIndicator(
|
||||
timeProvider: Clock, headerInterceptor: Preparable,
|
||||
private val statusProbe: StatusProbe
|
||||
) : AbstractHealthIndicator(timeProvider, headerInterceptor), HealthIndicator {
|
||||
override fun determineHealth(): Health {
|
||||
val healthBuilder = Health.status(statusProbe.status)
|
||||
if (statusProbe.lastStatusChange != null) {
|
||||
healthBuilder.withDetail(LAST_STATUS_CHANGE_KEY, statusProbe.lastStatusChange)
|
||||
}
|
||||
if (statusProbe.throwable != null) {
|
||||
healthBuilder.withException(statusProbe.throwable)
|
||||
}
|
||||
if (statusProbe.message != null) {
|
||||
healthBuilder.withDetail(MESSAGE_KEY, statusProbe.message)
|
||||
}
|
||||
return healthBuilder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MESSAGE_KEY = "message"
|
||||
const val LAST_STATUS_CHANGE_KEY = "lastStatusChange"
|
||||
}
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
package de.twomartens.timetable.support.monitoring.actuator
|
||||
|
||||
import java.io.Closeable
|
||||
|
||||
fun interface Preparable {
|
||||
fun prepare(): Closeable
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package de.twomartens.timetable.support.monitoring.actuator
|
||||
|
||||
import de.twomartens.timetable.property.ServiceProperties
|
||||
import de.twomartens.timetable.support.interceptor.HeaderInterceptorRest
|
||||
import org.springframework.boot.actuate.health.Health
|
||||
import org.springframework.boot.actuate.health.HealthIndicator
|
||||
import org.springframework.boot.actuate.health.Status
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.client.RestTemplate
|
||||
import java.security.SecureRandom
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* A Health check which checks if the rest services are working.
|
||||
*
|
||||
*
|
||||
* If you have a complex service, you should think about an easy greeting or echo service, which
|
||||
* only tests the
|
||||
* network/service stack and not the full application.
|
||||
*
|
||||
*
|
||||
* The health check will be called by kubernetes to check if the container/pod should be in load
|
||||
* balancing. It is possible
|
||||
* to have as much health checks as you like.
|
||||
*
|
||||
*
|
||||
* There should be a health check which is ok not before all data is loaded.
|
||||
*/
|
||||
@Component
|
||||
class RestHealthIndicator(
|
||||
clock: Clock, interceptor: HeaderInterceptorRest,
|
||||
serverProperties: ServerProperties,
|
||||
private val restTemplateRestHealthIndicator: RestTemplate,
|
||||
private val serviceProperties: ServiceProperties
|
||||
) : AbstractHealthIndicator(clock, Preparable { interceptor.markAsHealthCheck() }), HealthIndicator {
|
||||
private val randomizer = SecureRandom()
|
||||
private val urlPrefix: String = (HTTP_PREFIX + HOST + HOST_PORT_SEPARATOR
|
||||
+ serverProperties.port
|
||||
+ URL_PATH + PARAMETER_SEPARATOR + GET_PARAMETER)
|
||||
|
||||
/**
|
||||
* main method that determines the health of the service
|
||||
*/
|
||||
override fun determineHealth(): Health {
|
||||
val random = randomizer.nextInt(100000, 999999).toString()
|
||||
val url = "$urlPrefix{random}"
|
||||
val response = restTemplateRestHealthIndicator.getForEntity(url, String::class.java, random)
|
||||
val status = if (response.body == serviceProperties.greeting.format(random)) Status.UP else Status.DOWN
|
||||
return Health.status(status)
|
||||
.withDetail(DETAIL_ENDPOINT_KEY, url)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val URL_PATH = "/timetable/v1/healthCheck"
|
||||
private const val GET_PARAMETER = "message="
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user