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:
parent
b5681181fb
commit
7fbd270019
|
@ -7,9 +7,9 @@ tasks.withType<JavaCompile>().configureEach {
|
|||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
jvmArgs?.plusAssign("--enable-preview")
|
||||
jvmArgs.plusAssign("--enable-preview")
|
||||
}
|
||||
|
||||
tasks.withType<JavaExec>().configureEach {
|
||||
jvmArgs?.plusAssign("--enable-preview")
|
||||
jvmArgs.plusAssign("--enable-preview")
|
||||
}
|
|
@ -53,8 +53,10 @@ normalization.runtimeClasspath.metaInf {
|
|||
ignoreAttribute("Build-Timestamp")
|
||||
}
|
||||
|
||||
|
||||
|
||||
tasks.register("cleanLibs") {
|
||||
delete("${buildDir}/libs")
|
||||
delete("${layout.buildDirectory.get().asFile}/libs")
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
|
|
|
@ -16,6 +16,6 @@ tasks.named("build") {
|
|||
}
|
||||
|
||||
tasks.register("cleanCache") {
|
||||
delete("${buildDir}/jib-cache")
|
||||
delete("${buildDir}/libs")
|
||||
delete("${layout.buildDirectory.get().asFile}/jib-cache")
|
||||
delete("${layout.buildDirectory.get().asFile}/libs")
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ val projectSourceCompatibility: String = rootProject.properties["projectSourceCo
|
|||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xjvm-default=all")
|
||||
freeCompilerArgs.addAll("-Xjvm-default=all", "-Xjsr305=strict")
|
||||
jvmTarget.set(JvmTarget.fromTarget(projectSourceCompatibility))
|
||||
}
|
||||
}
|
|
@ -9,10 +9,10 @@ apply(plugin="com.netflix.nebula.release")
|
|||
tasks.register("writeVersionProperties") {
|
||||
group = "version"
|
||||
mustRunAfter("release")
|
||||
outputs.file("$buildDir/version.properties")
|
||||
val directory = buildDir
|
||||
outputs.file("${layout.buildDirectory.get().asFile}/version.properties")
|
||||
val directory = layout.buildDirectory.get().asFile
|
||||
doLast {
|
||||
Files.createDirectories(directory.toPath())
|
||||
File("$buildDir/version.properties").writeText("VERSION=${project.version}\n")
|
||||
File("${layout.buildDirectory.get().asFile}/version.properties").writeText("VERSION=${project.version}\n")
|
||||
}
|
||||
}
|
|
@ -1,18 +1,16 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeFormatter.ofPattern
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot")
|
||||
id("twomartens.java")
|
||||
id("twomartens.spring-boot-base")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.spring.boot))
|
||||
|
||||
implementation(libs.bundles.spring.boot)
|
||||
testImplementation(libs.spring.boot.test)
|
||||
}
|
||||
|
@ -37,13 +35,6 @@ val integrationTestImplementation: Configuration by configurations.getting {
|
|||
extendsFrom(configurations.testImplementation.get())
|
||||
}
|
||||
|
||||
configurations {
|
||||
configureEach {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
|
||||
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Test>("integrationTest") {
|
||||
systemProperty("junit.jupiter.execution.parallel.enabled", true)
|
||||
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
|
||||
|
@ -76,5 +67,9 @@ tasks.jar {
|
|||
|
||||
springBoot {
|
||||
buildInfo()
|
||||
mainClass.set("de.twomartens.timetable.MainApplicationKt")
|
||||
mainClass.set(project.properties["mainClass"].toString())
|
||||
}
|
||||
|
||||
tasks.named<BootJar>("bootJar") {
|
||||
enabled = true
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot")
|
||||
id("twomartens.java")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.spring.boot))
|
||||
implementation(libs.spring.boot.log4j)
|
||||
}
|
||||
|
||||
configurations {
|
||||
configureEach {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
|
||||
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<BootJar>("bootJar") {
|
||||
enabled = false
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
|
||||
plugins {
|
||||
id("twomartens.spring-boot")
|
||||
id("twomartens.spring-boot-application")
|
||||
id("twomartens.spring-boot-cloud-base")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.spring.cloud))
|
||||
implementation(libs.bundles.spring.boot.server)
|
||||
implementation(libs.spring.openapi)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
|
||||
plugins {
|
||||
id("twomartens.spring-boot-base")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.spring.cloud))
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
projectname=timetable
|
||||
projectgroup=de.2martens
|
||||
projectSourceCompatibility=21
|
||||
mainClass=de.twomartens.timetable.MainApplicationKt
|
||||
file.encoding=utf-8
|
||||
org.gradle.parallel=true
|
||||
org.gradle.daemon=true
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
plugins {
|
||||
id("twomartens.spring-boot-cloud-base")
|
||||
id("twomartens.kotlin")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation(project(":model"))
|
||||
implementation(project(":support"))
|
||||
|
||||
implementation(libs.mapstruct.base)
|
||||
annotationProcessor(libs.mapstruct.processor)
|
||||
kapt(libs.mapstruct.processor)
|
||||
|
||||
implementation(libs.jaxb.impl)
|
||||
implementation(libs.jakarta.xml.binding)
|
||||
|
||||
implementation(libs.spring.openapi)
|
||||
implementation(libs.spring.boot.mongo)
|
||||
implementation(libs.spring.cloud.leader.election)
|
||||
implementation(libs.spring.cloud.starter.bus.kafka)
|
||||
}
|
|
@ -13,7 +13,7 @@ open class ThreadPoolTaskSchedulerConfig {
|
|||
open fun threadPoolTaskScheduler(): ThreadPoolTaskScheduler {
|
||||
val scheduler = ThreadPoolTaskScheduler()
|
||||
scheduler.poolSize = POOL_SIZE
|
||||
scheduler.threadNamePrefix = THREAD_NAME_PREFIX
|
||||
scheduler.setThreadNamePrefix(THREAD_NAME_PREFIX)
|
||||
return scheduler
|
||||
}
|
||||
}
|
|
@ -11,11 +11,7 @@ class ScheduledTasksCreatedEvent private constructor(source: Instant, originServ
|
|||
destination: Destination)
|
||||
: RemoteApplicationEvent(source, originService, destination) {
|
||||
|
||||
private val creationTime: Instant
|
||||
|
||||
init {
|
||||
creationTime = source
|
||||
}
|
||||
private val creationTime: Instant = source
|
||||
|
||||
override fun getSource(): Instant {
|
||||
return creationTime
|
|
@ -1,6 +1,9 @@
|
|||
package de.twomartens.timetable.bahnApi.mapper
|
||||
|
||||
import de.twomartens.timetable.bahnApi.model.db.BahnStation
|
||||
import de.twomartens.timetable.model.common.CountryCode
|
||||
import de.twomartens.timetable.model.common.StationId
|
||||
import de.twomartens.timetable.model.db.Station
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import org.mapstruct.*
|
||||
|
||||
|
@ -22,4 +25,16 @@ interface BahnStationMapper {
|
|||
dto.db
|
||||
)
|
||||
}
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "lastModified", ignore = true)
|
||||
fun mapToCommonDB(db: BahnStation, countryCode: String): Station {
|
||||
return Station(
|
||||
StationId.of(NonEmptyString(countryCode + "-" + db.eva.value.toString())),
|
||||
CountryCode(NonEmptyString(countryCode)),
|
||||
db.name,
|
||||
listOf()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package de.twomartens.timetable.bahnApi.model
|
||||
|
||||
import de.twomartens.timetable.model.common.StationId
|
||||
|
||||
@JvmInline
|
||||
value class Eva(val value: Int) {
|
||||
init {
|
||||
|
@ -17,8 +15,8 @@ value class Eva(val value: Int) {
|
|||
companion object {
|
||||
val UNKNOWN = Eva(-1)
|
||||
|
||||
fun of(stationId: StationId): Eva {
|
||||
return Eva(stationId.stationIdWithinCountry.toInt())
|
||||
fun of(stationId: String): Eva {
|
||||
return Eva(stationId.toInt())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ import java.time.Instant
|
|||
import java.time.LocalDateTime
|
||||
|
||||
@Document
|
||||
@CompoundIndex(def = "{'eva': 1, 'fetchedDateTime': 1", unique = true)
|
||||
@CompoundIndex(def = "{'eva': 1, 'fetchedDateTime': 1}", unique = true)
|
||||
data class ScheduledFetchTask(
|
||||
var eva: Eva,
|
||||
var fetchedDateTime: HourAtDay,
|
|
@ -8,7 +8,7 @@ import jakarta.xml.bind.annotation.XmlRootElement
|
|||
@XmlRootElement(name = "stations")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
data class BahnStations(
|
||||
@field:XmlElement(name = "station") var stations: List<BahnStation>
|
||||
@field:XmlElement(name = "station") var stations: MutableList<BahnStation>
|
||||
) {
|
||||
constructor() : this(listOf())
|
||||
constructor() : this(mutableListOf())
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
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.MediaType
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.RestClient
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Service
|
||||
class BahnApiService(
|
||||
private val restClient: RestClient,
|
||||
private val properties: BahnApiProperties
|
||||
) {
|
||||
fun fetchStations(pattern: String): List<BahnStation> {
|
||||
val body = restClient.get()
|
||||
.uri("https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1/station/${pattern}")
|
||||
.headers {
|
||||
it.accept = mutableListOf(MediaType.APPLICATION_XML)
|
||||
it.contentType = MediaType.APPLICATION_XML
|
||||
it.set("DB-Client-Id", properties.clientId)
|
||||
it.set("DB-Api-Key", properties.clientSecret)
|
||||
}
|
||||
.retrieve()
|
||||
.body(BahnStations::class.java)
|
||||
return body?.stations ?: listOf()
|
||||
}
|
||||
|
||||
fun fetchTimetable(eva: Eva, hourAtDay: HourAtDay): BahnTimetable {
|
||||
val dateFormatter = DateTimeFormatter.ofPattern("yyMMdd")
|
||||
val timeFormatter = DateTimeFormatter.ofPattern("HH")
|
||||
val time = LocalTime.of(hourAtDay.hour.value, 0)
|
||||
val body = restClient.get()
|
||||
.uri("https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1/plan/" +
|
||||
"${eva}/${hourAtDay.date.format(dateFormatter)}/${time.format(timeFormatter)}")
|
||||
.headers {
|
||||
it.accept = mutableListOf(MediaType.APPLICATION_XML)
|
||||
it.contentType = MediaType.APPLICATION_XML
|
||||
it.set("DB-Client-Id", properties.clientId)
|
||||
it.set("DB-Api-Key", properties.clientSecret)
|
||||
}
|
||||
.retrieve()
|
||||
.body(BahnTimetable::class.java)
|
||||
return body!!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
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.model.db.Station
|
||||
import de.twomartens.timetable.model.repository.StationRepository
|
||||
import de.twomartens.timetable.types.HourAtDay
|
||||
import org.mapstruct.factory.Mappers
|
||||
import org.springframework.data.mongodb.core.BulkOperations
|
||||
import org.springframework.data.mongodb.core.MongoTemplate
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
open class BahnDatabaseService(
|
||||
private val bahnStationRepository: BahnStationRepository,
|
||||
private val stationRepository: StationRepository,
|
||||
private val bahnTimetableRepository: BahnTimetableRepository,
|
||||
private val mongoTemplate: MongoTemplate
|
||||
) {
|
||||
private val bahnStationMapper = Mappers.getMapper(BahnStationMapper::class.java)
|
||||
private val bahnTimetableMapper = Mappers.getMapper(BahnTimetableMapper::class.java)
|
||||
|
||||
fun storeStations(stations: List<BahnStation>) {
|
||||
|
||||
val existingStations = stationRepository.findAllByCountryCode(COUNTRY_CODE)
|
||||
val commonStationMap = existingStations
|
||||
.associateBy { it.stationId.stationIdWithinCountry }
|
||||
val bahnStationMap = stations.asSequence()
|
||||
.map(bahnStationMapper::mapToDB)
|
||||
.associateBy { it.eva.toString() }
|
||||
|
||||
updateBahnStations(bahnStationMap)
|
||||
deleteRemovedStations(existingStations, bahnStationMap)
|
||||
updateStations(existingStations, bahnStationMap)
|
||||
addNewStations(bahnStationMap, commonStationMap)
|
||||
}
|
||||
|
||||
private fun updateBahnStations(bahnStationMap: Map<String, de.twomartens.timetable.bahnApi.model.db.BahnStation>) {
|
||||
bahnStationRepository.deleteAll()
|
||||
val bahnStations: List<de.twomartens.timetable.bahnApi.model.db.BahnStation> = buildList {
|
||||
this.addAll(bahnStationMap.values)
|
||||
}
|
||||
|
||||
mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED,
|
||||
de.twomartens.timetable.bahnApi.model.db.BahnStation::class.java)
|
||||
.insert(bahnStations)
|
||||
.execute()
|
||||
}
|
||||
|
||||
private fun deleteRemovedStations(
|
||||
existingStations: List<Station>,
|
||||
bahnStationMap: Map<String, de.twomartens.timetable.bahnApi.model.db.BahnStation>
|
||||
) {
|
||||
val deletedStations = existingStations
|
||||
.filterNot { bahnStationMap.containsKey(it.stationId.stationIdWithinCountry) }
|
||||
stationRepository.deleteAll(deletedStations)
|
||||
}
|
||||
|
||||
private fun updateStations(
|
||||
existingStations: List<Station>,
|
||||
bahnStationMap: Map<String, de.twomartens.timetable.bahnApi.model.db.BahnStation>
|
||||
) {
|
||||
val updatedStations = existingStations
|
||||
.filter { bahnStationMap.containsKey(it.stationId.stationIdWithinCountry) }
|
||||
updatedStations.map {
|
||||
it.name = bahnStationMap[it.stationId.stationIdWithinCountry]!!.name
|
||||
}
|
||||
stationRepository.saveAll(updatedStations)
|
||||
}
|
||||
|
||||
private fun addNewStations(
|
||||
bahnStations: Map<String, de.twomartens.timetable.bahnApi.model.db.BahnStation>,
|
||||
commonStationMap: Map<String, Station>
|
||||
) {
|
||||
val newStations = bahnStations.asSequence()
|
||||
.filterNot { commonStationMap.containsKey(it.key) }
|
||||
.map { bahnStationMapper.mapToCommonDB(it.value, COUNTRY_CODE) }
|
||||
.toList()
|
||||
|
||||
mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED,
|
||||
Station::class.java)
|
||||
.insert(newStations)
|
||||
.execute()
|
||||
}
|
||||
|
||||
fun storeTimetable(timetable: BahnTimetable, hourAtDay: HourAtDay) {
|
||||
bahnTimetableRepository.save(bahnTimetableMapper.mapToDB(timetable, hourAtDay))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val COUNTRY_CODE = "de"
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ import java.time.ZonedDateTime
|
|||
private const val PAST_TASK_EXECUTION_OFFSET = 1
|
||||
|
||||
@Service
|
||||
class TaskScheduler(
|
||||
class FetchTaskScheduler(
|
||||
private val clock: Clock,
|
||||
private val threadPoolTaskScheduler: ThreadPoolTaskScheduler,
|
||||
private val threadPoolTaskExecutor: ThreadPoolTaskExecutor,
|
|
@ -1,5 +1,7 @@
|
|||
package de.twomartens.timetable.bahnApi.service
|
||||
|
||||
import de.twomartens.support.model.LeadershipStatus
|
||||
import de.twomartens.support.service.BusService
|
||||
import de.twomartens.timetable.bahnApi.events.ScheduledTasksCreatedEvent
|
||||
import de.twomartens.timetable.bahnApi.model.Eva
|
||||
import de.twomartens.timetable.bahnApi.model.FetchDates
|
||||
|
@ -7,8 +9,6 @@ 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
|
||||
|
@ -27,7 +27,7 @@ class ScheduledTaskService(
|
|||
private val leaderProperties: LeaderProperties,
|
||||
private val scheduledFetchTaskRepository: ScheduledFetchTaskRepository,
|
||||
private val taskFactory: TaskFactory,
|
||||
private val taskScheduler: TaskScheduler
|
||||
private val fetchTaskScheduler: FetchTaskScheduler
|
||||
) {
|
||||
private var createdTime: Instant = Instant.EPOCH
|
||||
private var lastUpdate: Instant = Instant.EPOCH
|
||||
|
@ -81,8 +81,8 @@ class ScheduledTaskService(
|
|||
fetchDates: FetchDates
|
||||
): List<ScheduledFetchTask> {
|
||||
val newTasks = mutableListOf<ScheduledFetchTask>()
|
||||
tswRoute.stationIds.forEach {
|
||||
val stationId = it
|
||||
tswRoute.stations.forEach {
|
||||
val stationId = it.id
|
||||
val eva = Eva.of(stationId)
|
||||
var hourAtDay = HourAtDay.of(Hour.of(23), fetchDates.previousDay)
|
||||
var newTask = taskFactory.createTaskAndUpdateCounter(eva, hourAtDay)
|
||||
|
@ -103,7 +103,7 @@ class ScheduledTaskService(
|
|||
|
||||
private fun scheduleTasksIfLeader(tasksToSchedule: List<ScheduledFetchTask>) {
|
||||
if (leadershipStatus.isLeader) {
|
||||
taskScheduler.scheduleFetchTasks(tasksToSchedule)
|
||||
fetchTaskScheduler.scheduleFetchTasks(tasksToSchedule)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ 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.bahnApi.service.FetchTaskScheduler
|
||||
import de.twomartens.timetable.types.HourAtDay
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
|
@ -12,7 +12,7 @@ open class FetchTimetableTask(
|
|||
private val eva: Eva,
|
||||
private val hourAtDay: HourAtDay,
|
||||
private val bahnApiService: BahnApiService,
|
||||
private val scheduler: TaskScheduler) : Runnable {
|
||||
private val scheduler: FetchTaskScheduler) : Runnable {
|
||||
override fun run() {
|
||||
log.info {
|
||||
"Fetch timetable: [eva: $eva], [date: ${hourAtDay.date}], [hour: ${hourAtDay.hour}]"
|
|
@ -0,0 +1,11 @@
|
|||
package de.twomartens.timetable.types
|
||||
|
||||
@JvmInline
|
||||
value class Email private constructor(val value: String) {
|
||||
companion object {
|
||||
fun of(email: NonEmptyString): Email {
|
||||
require(email.value.contains("@")) { "Invalid email format" }
|
||||
return Email(email.value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package de.twomartens.timetable.types
|
||||
|
||||
@JvmInline
|
||||
value class ZeroOrPositiveInteger(val value: Int) {
|
||||
init {
|
||||
require(value >= 0) {
|
||||
"Value must be zero or positive integer"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
plugins {
|
||||
id("twomartens.spring-boot-base")
|
||||
id("twomartens.kotlin")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation(libs.spring.boot.mongo)
|
||||
|
||||
implementation(libs.mapstruct.base)
|
||||
annotationProcessor(libs.mapstruct.processor)
|
||||
kapt(libs.mapstruct.processor)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.ZeroOrPositiveInteger
|
||||
|
||||
@JvmInline
|
||||
value class CoachCapacity(val capacity: ZeroOrPositiveInteger)
|
|
@ -0,0 +1,12 @@
|
|||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class DepotId private constructor(val id: String) {
|
||||
companion object {
|
||||
fun of(id: NonEmptyString): DepotId {
|
||||
return DepotId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class FormationId private constructor(val id: String) {
|
||||
companion object {
|
||||
fun of(id: NonEmptyString): FormationId {
|
||||
return FormationId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class PortalId private constructor(val id: String) {
|
||||
companion object {
|
||||
fun of(id: NonEmptyString): PortalId {
|
||||
return PortalId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class RouteId private constructor(val id: String) {
|
||||
companion object {
|
||||
fun of(id: NonEmptyString): RouteId {
|
||||
return RouteId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,8 +2,7 @@ package de.twomartens.timetable.model.common
|
|||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class StationId private constructor(val value: String) {
|
||||
class StationId private constructor(val value: String) {
|
||||
|
||||
companion object {
|
||||
private val idPattern = Regex("^\\w{2}-(?<countryStationId>\\w.*)")
|
|
@ -0,0 +1,12 @@
|
|||
package de.twomartens.timetable.model.common
|
||||
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
@JvmInline
|
||||
value class TimetableId private constructor(val value: String) {
|
||||
companion object {
|
||||
fun of(id: NonEmptyString): TimetableId {
|
||||
return TimetableId(id.value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.DepotId
|
||||
import de.twomartens.timetable.model.dto.Station
|
||||
import de.twomartens.timetable.model.dto.Track
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
data class Depot(
|
||||
val id: DepotId,
|
||||
val name: NonEmptyString,
|
||||
val nearestStation: Station,
|
||||
val tracks: List<Track>,
|
||||
val travelDurations: List<TravelDuration>
|
||||
)
|
|
@ -3,6 +3,7 @@ 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 de.twomartens.timetable.types.ZeroOrPositiveInteger
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.annotation.Id
|
||||
|
@ -17,8 +18,9 @@ data class Formation(
|
|||
var userId: UserId,
|
||||
var formationId: FormationId,
|
||||
var name: NonEmptyString,
|
||||
var trainSimWorldFormationId: FormationId,
|
||||
var coaches: List<NonEmptyString>
|
||||
var trainSimWorldFormationId: FormationId?,
|
||||
var formation: String,
|
||||
var length: ZeroOrPositiveInteger
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
|
@ -0,0 +1,12 @@
|
|||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.PortalId
|
||||
import de.twomartens.timetable.model.dto.Station
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
|
||||
data class Portal(
|
||||
val id: PortalId,
|
||||
val name: NonEmptyString,
|
||||
val nearestStation: Station,
|
||||
val travelDurations: List<TravelDuration>
|
||||
)
|
|
@ -1,6 +1,8 @@
|
|||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.*
|
||||
import de.twomartens.timetable.model.common.CountryCode
|
||||
import de.twomartens.timetable.model.common.Platform
|
||||
import de.twomartens.timetable.model.common.StationId
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
|
@ -11,14 +13,12 @@ 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,
|
||||
@CompoundIndex(def = "{stationId: 1, countryCode: 1}", unique = true)
|
||||
data class Station(
|
||||
var stationId: StationId,
|
||||
var countryCode: CountryCode,
|
||||
var stationIds: List<StationId>,
|
||||
var portals: List<Portal>,
|
||||
var depots: List<Depot>
|
||||
var name: NonEmptyString,
|
||||
var platforms: List<Platform>
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
|
@ -0,0 +1,39 @@
|
|||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.RouteId
|
||||
import de.twomartens.timetable.model.common.TimetableId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.dto.TimetableState
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import de.twomartens.timetable.types.ZeroOrPositiveInteger
|
||||
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.LocalDate
|
||||
|
||||
@Document
|
||||
@CompoundIndex(def = "{userId: 1, timetableId: 1}", unique = true)
|
||||
@CompoundIndex(def = "{userId: 1, routeId: 1}")
|
||||
data class Timetable(
|
||||
var userId: UserId,
|
||||
var routeId: RouteId,
|
||||
var routeName: String,
|
||||
var timetableId: TimetableId,
|
||||
var name: NonEmptyString,
|
||||
var fetchDate: LocalDate,
|
||||
var timetableState: TimetableState,
|
||||
var numberOfServices: ZeroOrPositiveInteger
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
||||
|
||||
@CreatedDate
|
||||
lateinit var created: Instant
|
||||
|
||||
@LastModifiedDate
|
||||
lateinit var lastModified: Instant
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.dto.Formation
|
||||
|
||||
data class TravelDuration(
|
||||
val formation: Formation,
|
||||
val time: Long
|
||||
)
|
|
@ -0,0 +1,35 @@
|
|||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.RouteId
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.model.dto.Country
|
||||
import de.twomartens.timetable.model.dto.Station
|
||||
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 routeId: RouteId,
|
||||
var name: NonEmptyString,
|
||||
var country: Country,
|
||||
var stations: List<Station>,
|
||||
var depots: List<Depot>,
|
||||
var portals: List<Portal>
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
||||
|
||||
@CreatedDate
|
||||
lateinit var created: Instant
|
||||
|
||||
@LastModifiedDate
|
||||
lateinit var lastModified: Instant
|
||||
}
|
|
@ -1,18 +1,21 @@
|
|||
package de.twomartens.timetable.model.db
|
||||
|
||||
import de.twomartens.timetable.model.common.UserId
|
||||
import de.twomartens.timetable.types.Email
|
||||
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.mapping.Document
|
||||
import java.time.Instant
|
||||
|
||||
@Document
|
||||
data class User(
|
||||
var userId: UserId,
|
||||
var name: NonEmptyString
|
||||
@Indexed(unique = true) var userId: UserId,
|
||||
var name: NonEmptyString,
|
||||
var email: Email
|
||||
) {
|
||||
@Id
|
||||
var id: ObjectId = ObjectId()
|
|
@ -1,14 +1,11 @@
|
|||
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 id: String,
|
||||
val line: String,
|
||||
val formationId: String,
|
||||
val direction: Direction,
|
||||
val start: Station,
|
||||
val destination: Station,
|
|
@ -1,12 +1,11 @@
|
|||
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 platformAndSection: String,
|
||||
val loading: Boolean
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class Country(
|
||||
val code: String,
|
||||
val name: String
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class Depot(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val nearestStation: Station,
|
||||
val tracks: List<Track>,
|
||||
val travelDurations: List<TravelDuration>
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class Formation(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val trainSimWorldFormation: Formation?,
|
||||
val formation: String,
|
||||
val length: Int
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class Portal(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val nearestStation: Station,
|
||||
val travelDurations: List<TravelDuration>
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import java.time.LocalTime
|
||||
|
||||
data class Rotation(
|
||||
val id: String,
|
||||
val formationId: String,
|
||||
val firstServiceStartTime: LocalTime,
|
||||
val lastServiceEndTime: LocalTime,
|
||||
val startsInVault: Boolean
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class ServiceFormationStage(
|
||||
val id: String,
|
||||
val line: String,
|
||||
val direction: Direction,
|
||||
val formation: String,
|
||||
val formationReversed: Boolean,
|
||||
val startStop: ServiceStop,
|
||||
val destinationStop: ServiceStop
|
||||
)
|
|
@ -1,13 +1,9 @@
|
|||
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 id: String,
|
||||
val formationId: String,
|
||||
val line: String,
|
||||
val direction: Direction,
|
||||
val formationReversed: Boolean,
|
||||
val startStop: ServiceStop,
|
|
@ -0,0 +1,15 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.Line
|
||||
import de.twomartens.timetable.model.common.ServiceId
|
||||
|
||||
data class ServiceRotationStage(
|
||||
val id: ServiceId,
|
||||
val line: Line,
|
||||
val originStop: String,
|
||||
val virtualDestinations: List<String>,
|
||||
val startingFrom: String,
|
||||
val endingIn: String,
|
||||
val serviceStartTime: String,
|
||||
val stops: List<ServiceStop>
|
||||
)
|
|
@ -1,11 +1,9 @@
|
|||
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 platform: String,
|
||||
val section: String
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import de.twomartens.timetable.model.common.Platform
|
||||
|
||||
data class Station(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val platforms: List<Platform>
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
data class Timetable(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val routeId: String,
|
||||
val routeName: String,
|
||||
val date: LocalDate,
|
||||
val state: TimetableState,
|
||||
val numberOfServices: Int
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class Track(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val capacity: Int
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class TravelDuration(
|
||||
val formation: Formation,
|
||||
val time: Long
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class TswRoute(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val country: Country,
|
||||
val stations: List<Station>,
|
||||
val firstStation: Station,
|
||||
val lastStation: Station,
|
||||
val numberOfStations: Int,
|
||||
val depots: List<Depot>,
|
||||
val portals: List<Portal>
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
package de.twomartens.timetable.model.dto
|
||||
|
||||
data class User(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val email: String
|
||||
)
|
|
@ -0,0 +1,36 @@
|
|||
package de.twomartens.timetable.model.mapper
|
||||
|
||||
import de.twomartens.timetable.model.common.CountryCode
|
||||
import de.twomartens.timetable.model.common.StationId
|
||||
import de.twomartens.timetable.model.dto.Station
|
||||
import de.twomartens.timetable.types.NonEmptyString
|
||||
import org.mapstruct.*
|
||||
|
||||
@Mapper(
|
||||
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
interface StationMapper {
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "lastModified", ignore = true)
|
||||
fun mapToDB(countryCode: CountryCode, dto: Station): de.twomartens.timetable.model.db.Station {
|
||||
return de.twomartens.timetable.model.db.Station(
|
||||
StationId.of(NonEmptyString(countryCode.countryCode.value + "-" + dto.id)),
|
||||
countryCode,
|
||||
NonEmptyString(dto.name),
|
||||
dto.platforms
|
||||
)
|
||||
}
|
||||
|
||||
fun mapStationsToDto(stations: List<de.twomartens.timetable.model.db.Station>): List<Station>
|
||||
|
||||
fun mapToDto(db: de.twomartens.timetable.model.db.Station): Station {
|
||||
return Station(
|
||||
db.stationId.stationIdWithinCountry,
|
||||
db.name.value,
|
||||
db.platforms
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package de.twomartens.timetable.model.repository
|
||||
|
||||
import de.twomartens.timetable.model.common.StationId
|
||||
import de.twomartens.timetable.model.db.Station
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.Aggregation
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
|
||||
interface StationRepository : MongoRepository<Station, ObjectId> {
|
||||
fun findByCountryCodeAndStationId(countryCode: String, stationId: StationId): Station?
|
||||
fun findAllByCountryCode(countryCode: String): List<Station>
|
||||
|
||||
@Aggregation(pipeline = [
|
||||
"{\$search: { index: \"stations\", returnStoredSource: true, compound: {must: [{phrase: {query: ?0, path: \"countryCode\"}},{autocomplete: {query: ?1,path: \"name\",tokenOrder: \"sequential\"}}]}}}",
|
||||
"{\$limit: 10}",
|
||||
"{\$lookup: { from: \"station\", localField: \"_id\", foreignField: \"_id\", as: \"document\" }}",
|
||||
"{\$unwind: \"\$document\"}",
|
||||
"{\$replaceWith: \"\$document\"}"
|
||||
])
|
||||
fun findAllByCountryCodeAndNameContainingIgnoreCase(countryCode: String, name: String): List<Station>
|
||||
|
||||
@Aggregation(pipeline = [
|
||||
"{\$search: { index: \"stations\", returnStoredSource: true, autocomplete: {query: ?0, path: \"name\",tokenOrder: \"sequential\"}}}",
|
||||
"{\$limit: 10}",
|
||||
"{\$lookup: { from: \"station\", localField: \"_id\", foreignField: \"_id\", as: \"document\" }}",
|
||||
"{\$unwind: \"\$document\"}",
|
||||
"{\$replaceWith: \"\$document\"}"
|
||||
])
|
||||
fun findAllByNameContainingIgnoreCase(name: String): List<Station>
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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,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
|
|
@ -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
|
||||
|
|
|
@ -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(
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue