From 0b8c602384a9ab657155bbdcbc6720a810d427fc Mon Sep 17 00:00:00 2001 From: Jim Martens Date: Sat, 23 Sep 2023 20:47:35 +0200 Subject: [PATCH] Implemented BahnAPI to fetch stations and timetables --- .../twomartens/timetable/MainApplication.kt | 2 +- .../bahnApi/jaxb/LocalDateTimeAdapter.kt | 29 ++++++++++ .../bahnApi/mapper/BahnStationMapper.kt | 17 ++++++ .../timetable/bahnApi/model/db/BahnStation.kt | 26 +++++++++ .../bahnApi/model/dto/BahnStation.kt | 19 +++++++ .../bahnApi/model/dto/BahnStationStop.kt | 17 ++++++ .../bahnApi/model/dto/BahnStations.kt | 14 +++++ .../bahnApi/model/dto/BahnStopEvent.kt | 32 +++++++++++ .../bahnApi/model/dto/BahnTimetable.kt | 13 +++++ .../bahnApi/model/dto/BahnTripLabel.kt | 18 ++++++ .../bahnApi/model/dto/BahnTripType.kt | 10 ++++ .../bahnApi/property/BahnApiProperties.kt | 14 +++++ .../repository/BahnStationRepository.kt | 6 ++ .../bahnApi/service/BahnApiService.kt | 57 +++++++++++++++++++ 14 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/jaxb/LocalDateTimeAdapter.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/mapper/BahnStationMapper.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnStation.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStation.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStationStop.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStations.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStopEvent.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTimetable.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripLabel.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripType.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/property/BahnApiProperties.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/repository/BahnStationRepository.kt create mode 100644 module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt b/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt index 6fd8825..dc3af78 100644 --- a/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt +++ b/module-server/src/main/kotlin/de/twomartens/timetable/MainApplication.kt @@ -22,7 +22,7 @@ open class MainApplication( fun ready() { val result = bahnApiService.fetchStations("Köln Hbf") val koeln = result[0] - val stops = bahnApiService.fetchTimetable(koeln.eva, + val timetable = bahnApiService.fetchTimetable(koeln.eva, LocalDate.of(2023, Month.SEPTEMBER, 23), LocalTime.of(17, 0)) log.info("stations: $result") diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/jaxb/LocalDateTimeAdapter.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/jaxb/LocalDateTimeAdapter.kt new file mode 100644 index 0000000..fe7408c --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/jaxb/LocalDateTimeAdapter.kt @@ -0,0 +1,29 @@ +package de.twomartens.timetable.bahnApi.jaxb + +import jakarta.xml.bind.annotation.adapters.XmlAdapter +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.* + + +class LocalDateTimeAdapter : XmlAdapter() { + + @Throws(Exception::class) + override fun unmarshal(xmlValue: String?): LocalDateTime { + return LocalDateTime.parse(xmlValue, dateFormatter.get()) + } + + @Throws(Exception::class) + override fun marshal(kotlinValue: LocalDateTime): String? { + return dateFormatter.get().format(kotlinValue) + } + + companion object { + private val dateFormatter: ThreadLocal = object : ThreadLocal() { + override fun initialValue(): DateTimeFormatter { + return DateTimeFormatter.ofPattern("yyMMddHHmm", Locale.GERMANY) + } + } + } + +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/mapper/BahnStationMapper.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/mapper/BahnStationMapper.kt new file mode 100644 index 0000000..d3028c7 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/mapper/BahnStationMapper.kt @@ -0,0 +1,17 @@ +package de.twomartens.timetable.bahnApi.mapper + +import de.twomartens.timetable.bahnApi.model.db.BahnStation +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 +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnStation.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnStation.kt new file mode 100644 index 0000000..03c7547 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/db/BahnStation.kt @@ -0,0 +1,26 @@ +package de.twomartens.timetable.bahnApi.model.db + +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 BahnStation( + var name: String, + @Indexed(unique = true) var eva: String, + var ds100: String, + var db: Boolean +) { + @Id + var id: ObjectId = ObjectId() + + @CreatedDate + lateinit var created: Instant + + @LastModifiedDate + lateinit var lastModified: Instant +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStation.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStation.kt new file mode 100644 index 0000000..a89d424 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStation.kt @@ -0,0 +1,19 @@ +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 BahnStation( + @field:XmlAttribute + var name: String, + @field:XmlAttribute + var eva: String, + @field:XmlAttribute + var ds100: String, + @field:XmlAttribute + var db: Boolean +) { + constructor(): this("", "", "", true) +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStationStop.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStationStop.kt new file mode 100644 index 0000000..912cf2b --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStationStop.kt @@ -0,0 +1,17 @@ +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 +import jakarta.xml.bind.annotation.XmlElement + +@XmlAccessorType(XmlAccessType.FIELD) +data class BahnStationStop( + @field:XmlAttribute var eva: Int?, + @field:XmlAttribute var id: String, + @field:XmlElement(name = "tl") var tripLabel: BahnTripLabel?, + @field:XmlElement(name = "ar") var arrival: BahnStopEvent?, + @field:XmlElement(name = "dp") var departure: BahnStopEvent? +) { + constructor(): this(null, "", null, null, null) +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStations.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStations.kt new file mode 100644 index 0000000..6a9ea4b --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStations.kt @@ -0,0 +1,14 @@ +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 +) { + constructor(): this(mutableListOf()) +} diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStopEvent.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStopEvent.kt new file mode 100644 index 0000000..2f348e3 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnStopEvent.kt @@ -0,0 +1,32 @@ +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, + "", "", "", "", + "", "") +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTimetable.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTimetable.kt new file mode 100644 index 0000000..45645a2 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTimetable.kt @@ -0,0 +1,13 @@ +package de.twomartens.timetable.bahnApi.model.dto + +import jakarta.xml.bind.annotation.* + +@XmlRootElement(name = "timetable") +@XmlAccessorType(XmlAccessType.FIELD) +data class BahnTimetable( + @field:XmlAttribute var eva: Int?, + @field:XmlAttribute var station: String, + @field:XmlElement(name = "s") var stops: List +) { + constructor(): this(null, "", mutableListOf()) +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripLabel.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripLabel.kt new file mode 100644 index 0000000..47b8418 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripLabel.kt @@ -0,0 +1,18 @@ +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("", "", "", "", + null) +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripType.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripType.kt new file mode 100644 index 0000000..a89fb71 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/model/dto/BahnTripType.kt @@ -0,0 +1,10 @@ +package de.twomartens.timetable.bahnApi.model.dto + +enum class BahnTripType { + p, // passenger? + e, + z, + s, + h, + n +} \ No newline at end of file diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/property/BahnApiProperties.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/property/BahnApiProperties.kt new file mode 100644 index 0000000..109fa8a --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/property/BahnApiProperties.kt @@ -0,0 +1,14 @@ +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 +) diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/repository/BahnStationRepository.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/repository/BahnStationRepository.kt new file mode 100644 index 0000000..ed1285d --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/repository/BahnStationRepository.kt @@ -0,0 +1,6 @@ +package de.twomartens.timetable.bahnApi.repository + +import de.twomartens.timetable.bahnApi.model.db.BahnStation +import org.springframework.data.mongodb.repository.MongoRepository + +interface BahnStationRepository : MongoRepository diff --git a/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt new file mode 100644 index 0000000..57073e5 --- /dev/null +++ b/module-server/src/main/kotlin/de/twomartens/timetable/bahnApi/service/BahnApiService.kt @@ -0,0 +1,57 @@ +package de.twomartens.timetable.bahnApi.service + +import de.twomartens.timetable.bahnApi.model.dto.BahnStation +import de.twomartens.timetable.bahnApi.model.dto.BahnStationStop +import de.twomartens.timetable.bahnApi.model.dto.BahnStations +import de.twomartens.timetable.bahnApi.model.dto.BahnTimetable +import de.twomartens.timetable.bahnApi.property.BahnApiProperties +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate +import java.time.LocalDate +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@Service +class BahnApiService( + private val restTemplate: RestTemplate, + private val properties: BahnApiProperties +) { + fun fetchStations(pattern: String): List { + val requestEntity = buildRequestEntity() + 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: String, date: LocalDate, hour: LocalTime): BahnTimetable { + val requestEntity = buildRequestEntity() + val dateFormatter = DateTimeFormatter.ofPattern("yyMMdd") + val timeFormatter = DateTimeFormatter.ofPattern("HH") + val response = restTemplate.exchange( + "https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1/plan/" + + "${eva}/${date.format(dateFormatter)}/${hour.format(timeFormatter)}", + HttpMethod.GET, + requestEntity, + BahnTimetable::class.java + ) + return response.body!! + } + + private fun buildRequestEntity(): HttpEntity { + 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(headers) + } +} \ No newline at end of file