From 3896d440ad204ddee298e10bce978074bd44d426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Wittner?= <wittner.balazs@microsec.hu> Date: Wed, 20 Mar 2024 08:47:21 +0100 Subject: [PATCH 1/3] 1st go --- app/build.gradle.kts | 12 + app/src/main/kotlin/WeatherApp.kt | 7 +- app/src/main/kotlin/WeatherAppConfig.kt | 8 + .../main/kotlin/WeatherCommandLineRunner.kt | 24 ++ .../main/kotlin/openmeteo/WeatherForecast.kt | 31 ++ .../main/kotlin/openmeteo/WeatherForecasts.kt | 18 + .../main/kotlin/openmeteo/WeatherService.kt | 45 ++ app/src/main/kotlin/vaadin/MainView.kt | 38 ++ app/src/main/resources/application.properties | 12 + .../openmeteo/WeatherMeanTimeServiceTest.kt | 394 ++++++++++++++++++ 10 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/WeatherAppConfig.kt create mode 100644 app/src/main/kotlin/WeatherCommandLineRunner.kt create mode 100644 app/src/main/kotlin/openmeteo/WeatherForecast.kt create mode 100644 app/src/main/kotlin/openmeteo/WeatherForecasts.kt create mode 100644 app/src/main/kotlin/openmeteo/WeatherService.kt create mode 100644 app/src/main/kotlin/vaadin/MainView.kt create mode 100644 app/src/main/resources/application.properties create mode 100644 app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1182c84..42d7eaf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,24 +1,32 @@ +import java.net.URI + plugins { val kotlinVersion = "1.9.23" kotlin("jvm") version kotlinVersion kotlin("plugin.spring") version kotlinVersion id("org.springframework.boot") version "3.2.3" + id("com.vaadin") version "24.3.7" } group = "hu.kotlin.feladat.ms" repositories { mavenCentral() + maven { url = URI("https://maven.vaadin.com/vaadin-addons") } } dependencies { implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("com.vaadin:vaadin-core:24.+") + implementation("com.vaadin:vaadin-spring-boot-starter:24.+") testImplementation(kotlin("test")) testImplementation("io.mockk:mockk:1.4.1") } +defaultTasks("clean", "vaadinBuildFrontend", "build") + tasks.test { useJUnitPlatform() } @@ -26,3 +34,7 @@ tasks.test { kotlin { jvmToolchain(17) } + +vaadin { + optimizeBundle = false +} \ No newline at end of file diff --git a/app/src/main/kotlin/WeatherApp.kt b/app/src/main/kotlin/WeatherApp.kt index e0c098a..aa04459 100644 --- a/app/src/main/kotlin/WeatherApp.kt +++ b/app/src/main/kotlin/WeatherApp.kt @@ -1,10 +1,15 @@ package hu.vanio.kotlin.feladat.ms +import com.vaadin.flow.component.page.AppShellConfigurator +import com.vaadin.flow.theme.Theme import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication @SpringBootApplication -class WeatherApp +@EnableConfigurationProperties +@Theme +class WeatherApp: AppShellConfigurator fun main() { runApplication<WeatherApp>() diff --git a/app/src/main/kotlin/WeatherAppConfig.kt b/app/src/main/kotlin/WeatherAppConfig.kt new file mode 100644 index 0000000..845e47d --- /dev/null +++ b/app/src/main/kotlin/WeatherAppConfig.kt @@ -0,0 +1,8 @@ +package hu.vanio.kotlin.feladat.ms + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Configuration + +@Configuration +@ConfigurationProperties(prefix = "weather") +class WeatherAppConfig(var appUrl: String = "") diff --git a/app/src/main/kotlin/WeatherCommandLineRunner.kt b/app/src/main/kotlin/WeatherCommandLineRunner.kt new file mode 100644 index 0000000..9306531 --- /dev/null +++ b/app/src/main/kotlin/WeatherCommandLineRunner.kt @@ -0,0 +1,24 @@ +package hu.vanio.kotlin.feladat.ms + +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecasts +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService +import org.springframework.boot.CommandLineRunner +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component + + +const val separator = "----------------------------------------------" + +@Component +@Order(1) +class WeatherCommandLineRunner(val weatherService: WeatherService): CommandLineRunner { + + override fun run(vararg args: String?) { + print("$separator\n${weatherService.getWeatherStatistics().print()}\n$separator\n") + } +} + +fun WeatherForecasts.print() = + "Weather forecast daily averages:\n$separator\n${this.weatherDailyForecast + .map { wdf -> "${wdf.day} | ${wdf.average()}"} + .reduce { first, second -> "${first}\n${second}"}}" \ No newline at end of file diff --git a/app/src/main/kotlin/openmeteo/WeatherForecast.kt b/app/src/main/kotlin/openmeteo/WeatherForecast.kt new file mode 100644 index 0000000..90584fd --- /dev/null +++ b/app/src/main/kotlin/openmeteo/WeatherForecast.kt @@ -0,0 +1,31 @@ +package hu.vanio.kotlin.feladat.ms.openmeteo + +import com.fasterxml.jackson.annotation.JsonProperty + +data class WeatherForecast ( + val latitude: Double, + val longitude: Double, + @JsonProperty("generationtime_ms") + val generationtimeMs: Double, + @JsonProperty("utc_offset_seconds") + val utcOffsetSeconds: Long, + val timezone: String, + @JsonProperty("timezone_abbreviation") + val timezoneAbbreviation: String, + val elevation: Double, + @JsonProperty("hourly_units") + val hourlyUnits: HourlyUnits, + val hourly: Hourly, +) + +data class HourlyUnits( + val time: String, + @JsonProperty("temperature_2m") + val temperature2m: String, +) + +data class Hourly( + val time: List<String>, + @JsonProperty("temperature_2m") + val temperature2m: List<Double>, +) \ No newline at end of file diff --git a/app/src/main/kotlin/openmeteo/WeatherForecasts.kt b/app/src/main/kotlin/openmeteo/WeatherForecasts.kt new file mode 100644 index 0000000..7e8526b --- /dev/null +++ b/app/src/main/kotlin/openmeteo/WeatherForecasts.kt @@ -0,0 +1,18 @@ +package hu.vanio.kotlin.feladat.ms.openmeteo + +import java.math.BigDecimal +import java.math.RoundingMode +import java.time.LocalDate + +data class WeatherDailyForecast(val day: LocalDate, private val temperatures: List<Double>) { + fun average(): Double = temperatures.average().round(2) + + override fun toString(): String { + return "$day - ${average()}" + } +} + +fun Double.round(digits: Int) = + BigDecimal(this).setScale(digits, RoundingMode.HALF_EVEN).toDouble() + +data class WeatherForecasts(val weatherDailyForecast: List<WeatherDailyForecast>) \ No newline at end of file diff --git a/app/src/main/kotlin/openmeteo/WeatherService.kt b/app/src/main/kotlin/openmeteo/WeatherService.kt new file mode 100644 index 0000000..3acb9b7 --- /dev/null +++ b/app/src/main/kotlin/openmeteo/WeatherService.kt @@ -0,0 +1,45 @@ +package hu.vanio.kotlin.feladat.ms.openmeteo + +import com.fasterxml.jackson.databind.ObjectMapper +import hu.vanio.kotlin.feladat.ms.WeatherAppConfig +import org.springframework.stereotype.Service + +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +@Service +class WeatherService(var config: WeatherAppConfig, var objectMapper: ObjectMapper) { + fun getWeatherStatistics(): WeatherForecasts { + val request = HttpRequest.newBuilder() + .uri(URI(config.appUrl)) + .GET() + .build() + + val weatherForecast = objectMapper.readValue(HttpClient.newHttpClient() + .send(request, HttpResponse.BodyHandlers.ofString()).body(), WeatherForecast::class.java) + return statistics(weatherForecast) + } + + companion object { + fun statistics(weatherForecast: WeatherForecast): WeatherForecasts { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm") + val weatherMap = mutableMapOf<LocalDate, MutableList<Double>>() + var i = 0 + + weatherForecast.hourly.time.forEach { + val temperature = weatherForecast.hourly.temperature2m[i++] + val key = LocalDate.parse(it, dateTimeFormatter) + weatherMap.compute(key) { _, value -> + value?.plus(temperature)?.toMutableList() ?: mutableListOf(temperature) + } + } + return WeatherForecasts(weatherMap.map { + WeatherDailyForecast(it.key, it.value) + }.toList()) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/vaadin/MainView.kt b/app/src/main/kotlin/vaadin/MainView.kt new file mode 100644 index 0000000..f5361e2 --- /dev/null +++ b/app/src/main/kotlin/vaadin/MainView.kt @@ -0,0 +1,38 @@ +package hu.vanio.kotlin.feladat.ms.vaadin + +import com.vaadin.flow.component.Key +import com.vaadin.flow.component.button.Button +import com.vaadin.flow.component.grid.Grid +import com.vaadin.flow.component.notification.Notification +import com.vaadin.flow.component.orderedlayout.FlexComponent +import com.vaadin.flow.component.orderedlayout.HorizontalLayout +import com.vaadin.flow.router.PageTitle +import com.vaadin.flow.router.Route +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherDailyForecast +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService + +@PageTitle("Weather forecast") +@Route +class MainView(private val weatherService: WeatherService) : HorizontalLayout() { + private val table = Grid<WeatherDailyForecast>() + private val refresh = Button("Refresh").also { + it.addClickListener { _ -> + refreshTableContent() + Notification.show("Table content refreshed") } + it.addClickShortcut(Key.ENTER) + } + + private fun refreshTableContent() { + val weatherStatistics = weatherService.getWeatherStatistics() + table.setItems(weatherStatistics.weatherDailyForecast) + } + + init { + table.addColumn(WeatherDailyForecast::day).setHeader("Day") + table.addColumn(WeatherDailyForecast::average).setHeader("Daily average temperature") + + super.setMargin(true) + super.setVerticalComponentAlignment(FlexComponent.Alignment.END, table, refresh) + super.add(table, refresh) + } +} \ No newline at end of file diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties new file mode 100644 index 0000000..5517427 --- /dev/null +++ b/app/src/main/resources/application.properties @@ -0,0 +1,12 @@ +weather.app-url=https://api.open-meteo.com/v1/forecast?latitude=47.4984&longitude=19.0404&hourly=temperature_2m&timezone=auto + +server.port=${PORT:8080} + +spring.mustache.check-template-location = false + +# Launch the default browser when starting the application in development mode +vaadin.launch-browser=true +# To improve the performance during development. +# For more information https://vaadin.com/docs/latest/integrations/spring/configuration#special-configuration-parameters +vaadin.allowed-packages = com.vaadin,org.vaadin,dev.hilla,com.example.application +spring.jpa.defer-datasource-initialization = true diff --git a/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt b/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt new file mode 100644 index 0000000..d23ea17 --- /dev/null +++ b/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt @@ -0,0 +1,394 @@ +package openmeteo + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecast +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class WeatherMeanTimeServiceTest() { + private val objectMapper = ObjectMapper().also { + it.registerModules(JavaTimeModule(), ParameterNamesModule()) + it.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false) + } + + val response = """ + { + "latitude": 47.5, + "longitude": 19.0625, + "generationtime_ms": 0.030040740966796875, + "utc_offset_seconds": 3600, + "timezone": "Europe/Budapest", + "timezone_abbreviation": "CET", + "elevation": 124, + "hourly_units": { + "time": "iso8601", + "temperature_2m": "°C" + }, + "hourly": { + "time": [ + "2024-03-18T00:00", + "2024-03-18T01:00", + "2024-03-18T02:00", + "2024-03-18T03:00", + "2024-03-18T04:00", + "2024-03-18T05:00", + "2024-03-18T06:00", + "2024-03-18T07:00", + "2024-03-18T08:00", + "2024-03-18T09:00", + "2024-03-18T10:00", + "2024-03-18T11:00", + "2024-03-18T12:00", + "2024-03-18T13:00", + "2024-03-18T14:00", + "2024-03-18T15:00", + "2024-03-18T16:00", + "2024-03-18T17:00", + "2024-03-18T18:00", + "2024-03-18T19:00", + "2024-03-18T20:00", + "2024-03-18T21:00", + "2024-03-18T22:00", + "2024-03-18T23:00", + "2024-03-19T00:00", + "2024-03-19T01:00", + "2024-03-19T02:00", + "2024-03-19T03:00", + "2024-03-19T04:00", + "2024-03-19T05:00", + "2024-03-19T06:00", + "2024-03-19T07:00", + "2024-03-19T08:00", + "2024-03-19T09:00", + "2024-03-19T10:00", + "2024-03-19T11:00", + "2024-03-19T12:00", + "2024-03-19T13:00", + "2024-03-19T14:00", + "2024-03-19T15:00", + "2024-03-19T16:00", + "2024-03-19T17:00", + "2024-03-19T18:00", + "2024-03-19T19:00", + "2024-03-19T20:00", + "2024-03-19T21:00", + "2024-03-19T22:00", + "2024-03-19T23:00", + "2024-03-20T00:00", + "2024-03-20T01:00", + "2024-03-20T02:00", + "2024-03-20T03:00", + "2024-03-20T04:00", + "2024-03-20T05:00", + "2024-03-20T06:00", + "2024-03-20T07:00", + "2024-03-20T08:00", + "2024-03-20T09:00", + "2024-03-20T10:00", + "2024-03-20T11:00", + "2024-03-20T12:00", + "2024-03-20T13:00", + "2024-03-20T14:00", + "2024-03-20T15:00", + "2024-03-20T16:00", + "2024-03-20T17:00", + "2024-03-20T18:00", + "2024-03-20T19:00", + "2024-03-20T20:00", + "2024-03-20T21:00", + "2024-03-20T22:00", + "2024-03-20T23:00", + "2024-03-21T00:00", + "2024-03-21T01:00", + "2024-03-21T02:00", + "2024-03-21T03:00", + "2024-03-21T04:00", + "2024-03-21T05:00", + "2024-03-21T06:00", + "2024-03-21T07:00", + "2024-03-21T08:00", + "2024-03-21T09:00", + "2024-03-21T10:00", + "2024-03-21T11:00", + "2024-03-21T12:00", + "2024-03-21T13:00", + "2024-03-21T14:00", + "2024-03-21T15:00", + "2024-03-21T16:00", + "2024-03-21T17:00", + "2024-03-21T18:00", + "2024-03-21T19:00", + "2024-03-21T20:00", + "2024-03-21T21:00", + "2024-03-21T22:00", + "2024-03-21T23:00", + "2024-03-22T00:00", + "2024-03-22T01:00", + "2024-03-22T02:00", + "2024-03-22T03:00", + "2024-03-22T04:00", + "2024-03-22T05:00", + "2024-03-22T06:00", + "2024-03-22T07:00", + "2024-03-22T08:00", + "2024-03-22T09:00", + "2024-03-22T10:00", + "2024-03-22T11:00", + "2024-03-22T12:00", + "2024-03-22T13:00", + "2024-03-22T14:00", + "2024-03-22T15:00", + "2024-03-22T16:00", + "2024-03-22T17:00", + "2024-03-22T18:00", + "2024-03-22T19:00", + "2024-03-22T20:00", + "2024-03-22T21:00", + "2024-03-22T22:00", + "2024-03-22T23:00", + "2024-03-23T00:00", + "2024-03-23T01:00", + "2024-03-23T02:00", + "2024-03-23T03:00", + "2024-03-23T04:00", + "2024-03-23T05:00", + "2024-03-23T06:00", + "2024-03-23T07:00", + "2024-03-23T08:00", + "2024-03-23T09:00", + "2024-03-23T10:00", + "2024-03-23T11:00", + "2024-03-23T12:00", + "2024-03-23T13:00", + "2024-03-23T14:00", + "2024-03-23T15:00", + "2024-03-23T16:00", + "2024-03-23T17:00", + "2024-03-23T18:00", + "2024-03-23T19:00", + "2024-03-23T20:00", + "2024-03-23T21:00", + "2024-03-23T22:00", + "2024-03-23T23:00", + "2024-03-24T00:00", + "2024-03-24T01:00", + "2024-03-24T02:00", + "2024-03-24T03:00", + "2024-03-24T04:00", + "2024-03-24T05:00", + "2024-03-24T06:00", + "2024-03-24T07:00", + "2024-03-24T08:00", + "2024-03-24T09:00", + "2024-03-24T10:00", + "2024-03-24T11:00", + "2024-03-24T12:00", + "2024-03-24T13:00", + "2024-03-24T14:00", + "2024-03-24T15:00", + "2024-03-24T16:00", + "2024-03-24T17:00", + "2024-03-24T18:00", + "2024-03-24T19:00", + "2024-03-24T20:00", + "2024-03-24T21:00", + "2024-03-24T22:00", + "2024-03-24T23:00" + ], + "temperature_2m": [ + 6, + 5.7, + 5.4, + 5.2, + 4.8, + 4.6, + 4.5, + 4.7, + 5.9, + 7.3, + 8.9, + 9.7, + 10.4, + 10.9, + 11.1, + 11, + 10.6, + 9.8, + 8.7, + 7.4, + 6.2, + 5.4, + 4.7, + 4.2, + 3.7, + 3.3, + 3, + 2.9, + 2.8, + 2.8, + 2.8, + 3.2, + 3.9, + 4.7, + 5.5, + 6.4, + 7.4, + 8, + 8.5, + 8.8, + 8.8, + 8.6, + 7.8, + 6.7, + 5.6, + 4.7, + 4, + 3.4, + 2.8, + 2.3, + 1.9, + 1.6, + 1.3, + 1, + 0.8, + 1.1, + 3.5, + 6.8, + 9.1, + 10.5, + 11.5, + 12.2, + 12.8, + 13.1, + 12.9, + 12.3, + 10.9, + 9.1, + 7.8, + 6.9, + 6, + 5.4, + 4.6, + 4.3, + 4.1, + 4.1, + 4.2, + 4.3, + 4.1, + 4.1, + 6.1, + 8.3, + 10.1, + 11.5, + 12.6, + 13.3, + 13.9, + 14.2, + 14.1, + 13.4, + 12.4, + 11.4, + 10.7, + 10.1, + 9.5, + 9.1, + 8.8, + 8.5, + 8.2, + 8, + 7.8, + 7.4, + 7, + 7.2, + 8.2, + 9.6, + 11.2, + 12.7, + 14.3, + 15.5, + 16.2, + 16.4, + 16.1, + 15.3, + 14, + 12.7, + 11.7, + 10.7, + 9.9, + 9.4, + 9.1, + 8.9, + 8.7, + 8.5, + 8.3, + 8.1, + 7.9, + 7.7, + 10.8, + 12.7, + 14.5, + 16.4, + 18.3, + 19.7, + 20.4, + 20.6, + 20.3, + 19.5, + 18.2, + 16.7, + 14.8, + 12.7, + 10.9, + 9.9, + 9.3, + 8.7, + 8.2, + 7.8, + 7.5, + 7.2, + 7, + 6.9, + 7.1, + 7.5, + 7.8, + 7.8, + 7.6, + 7.5, + 7.4, + 7.4, + 7.4, + 7.2, + 6.9, + 6.8, + 6.8, + 7, + 7.1, + 7.1 + ] + } +} + """.trimIndent() + + @Test + fun test() { + val weatherForecast = objectMapper.readValue(response, WeatherForecast::class.java) + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm") + val weatherMap = mutableMapOf<LocalDate, MutableList<Double>>() + + var i = 0 + weatherForecast.hourly.time.forEach { + val temperature = weatherForecast.hourly.temperature2m[i++] + val key = LocalDate.parse(it, dateTimeFormatter) + weatherMap.compute(key) { _, value -> + value?.plus(temperature)?.toMutableList() ?: mutableListOf(temperature) + } + } + weatherMap.forEach { + println("${it.key} - ${it.value.average()}") + } + } +} \ No newline at end of file From 8023b88551917a19a0eebb22609cadb905d248e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Wittner?= <wittner.balazs@microsec.hu> Date: Wed, 20 Mar 2024 10:10:38 +0100 Subject: [PATCH 2/3] Error handling --- .../main/kotlin/WeatherCommandLineRunner.kt | 9 +++++++-- .../kotlin/exception/ServiceUnavailable.kt | 6 ++++++ .../main/kotlin/openmeteo/WeatherService.kt | 10 ++++++++-- .../main/kotlin/vaadin/CustomErrorHandler.kt | 18 ++++++++++++++++++ .../main/kotlin/vaadin/VaadinInitListener.kt | 13 +++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 app/src/main/kotlin/exception/ServiceUnavailable.kt create mode 100644 app/src/main/kotlin/vaadin/CustomErrorHandler.kt create mode 100644 app/src/main/kotlin/vaadin/VaadinInitListener.kt diff --git a/app/src/main/kotlin/WeatherCommandLineRunner.kt b/app/src/main/kotlin/WeatherCommandLineRunner.kt index 9306531..40361ab 100644 --- a/app/src/main/kotlin/WeatherCommandLineRunner.kt +++ b/app/src/main/kotlin/WeatherCommandLineRunner.kt @@ -1,5 +1,6 @@ package hu.vanio.kotlin.feladat.ms +import hu.vanio.kotlin.feladat.ms.exception.ServiceUnavailable import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecasts import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService import org.springframework.boot.CommandLineRunner @@ -14,11 +15,15 @@ const val separator = "----------------------------------------------" class WeatherCommandLineRunner(val weatherService: WeatherService): CommandLineRunner { override fun run(vararg args: String?) { - print("$separator\n${weatherService.getWeatherStatistics().print()}\n$separator\n") + val weatherStatistics = try { weatherService.getWeatherStatistics() } catch (e: ServiceUnavailable) { + println("Weather forecast service is unavailable") + return + } + println("$separator\n${weatherStatistics.println()}$separator") } } -fun WeatherForecasts.print() = +fun WeatherForecasts.println() = "Weather forecast daily averages:\n$separator\n${this.weatherDailyForecast .map { wdf -> "${wdf.day} | ${wdf.average()}"} .reduce { first, second -> "${first}\n${second}"}}" \ No newline at end of file diff --git a/app/src/main/kotlin/exception/ServiceUnavailable.kt b/app/src/main/kotlin/exception/ServiceUnavailable.kt new file mode 100644 index 0000000..ba5ec5b --- /dev/null +++ b/app/src/main/kotlin/exception/ServiceUnavailable.kt @@ -0,0 +1,6 @@ +package hu.vanio.kotlin.feladat.ms.exception + +class ServiceUnavailable(private val serviceId: String): RuntimeException() { + override val message: String + get() = "Service $serviceId is unavailable" +} \ No newline at end of file diff --git a/app/src/main/kotlin/openmeteo/WeatherService.kt b/app/src/main/kotlin/openmeteo/WeatherService.kt index 3acb9b7..569dae1 100644 --- a/app/src/main/kotlin/openmeteo/WeatherService.kt +++ b/app/src/main/kotlin/openmeteo/WeatherService.kt @@ -2,7 +2,9 @@ package hu.vanio.kotlin.feladat.ms.openmeteo import com.fasterxml.jackson.databind.ObjectMapper import hu.vanio.kotlin.feladat.ms.WeatherAppConfig +import hu.vanio.kotlin.feladat.ms.exception.ServiceUnavailable import org.springframework.stereotype.Service +import java.io.IOException import java.net.URI import java.net.http.HttpClient @@ -13,14 +15,18 @@ import java.time.format.DateTimeFormatter @Service class WeatherService(var config: WeatherAppConfig, var objectMapper: ObjectMapper) { + private val id = "WEATHER_SERVICE" + fun getWeatherStatistics(): WeatherForecasts { val request = HttpRequest.newBuilder() .uri(URI(config.appUrl)) .GET() .build() - - val weatherForecast = objectMapper.readValue(HttpClient.newHttpClient() + val weatherForecast = try {objectMapper.readValue(HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()).body(), WeatherForecast::class.java) + } catch (e: IOException) { + throw ServiceUnavailable(id) + } return statistics(weatherForecast) } diff --git a/app/src/main/kotlin/vaadin/CustomErrorHandler.kt b/app/src/main/kotlin/vaadin/CustomErrorHandler.kt new file mode 100644 index 0000000..3b3f3d8 --- /dev/null +++ b/app/src/main/kotlin/vaadin/CustomErrorHandler.kt @@ -0,0 +1,18 @@ +package hu.vanio.kotlin.feladat.ms.vaadin + +import com.vaadin.flow.component.UI +import com.vaadin.flow.component.notification.Notification +import com.vaadin.flow.server.ErrorEvent +import com.vaadin.flow.server.ErrorHandler +import org.springframework.stereotype.Component + +@Component +class CustomErrorHandler : ErrorHandler { + override fun error(errorEvent: ErrorEvent) { + if (UI.getCurrent() != null) { + UI.getCurrent().access { + Notification.show("An internal error has occurred. (${errorEvent.throwable.message})") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/vaadin/VaadinInitListener.kt b/app/src/main/kotlin/vaadin/VaadinInitListener.kt new file mode 100644 index 0000000..d51f747 --- /dev/null +++ b/app/src/main/kotlin/vaadin/VaadinInitListener.kt @@ -0,0 +1,13 @@ +package hu.vanio.kotlin.feladat.ms.vaadin + +import com.vaadin.flow.server.ServiceInitEvent +import com.vaadin.flow.server.SessionInitEvent +import com.vaadin.flow.server.VaadinServiceInitListener +import org.springframework.stereotype.Component + +@Component +class VaadinInitListener(val customErrorHandler: CustomErrorHandler) : VaadinServiceInitListener { + override fun serviceInit(serviceEvent: ServiceInitEvent) { + serviceEvent.source.addSessionInitListener { initEvent: SessionInitEvent? -> initEvent?.session?.errorHandler = customErrorHandler} + } +} \ No newline at end of file From 36e53c5c71ab2260825ce723f0fbba7a62f68613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Wittner?= <wittner.balazs@microsec.hu> Date: Wed, 20 Mar 2024 12:49:01 +0100 Subject: [PATCH 3/3] Tests --- app/build.gradle.kts | 3 +- .../main/kotlin/WeatherCommandLineRunner.kt | 2 +- app/src/test/kotlin/WeatherAppTest.kt | 11 - .../kotlin/WeatherCommandLineRunnerTest.kt | 61 +++ .../openmeteo/WeatherMeanTimeServiceTest.kt | 394 ------------------ .../kotlin/openmeteo/WeatherServiceTest.kt | 133 ++++++ 6 files changed, 197 insertions(+), 407 deletions(-) delete mode 100644 app/src/test/kotlin/WeatherAppTest.kt create mode 100644 app/src/test/kotlin/WeatherCommandLineRunnerTest.kt delete mode 100644 app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt create mode 100644 app/src/test/kotlin/openmeteo/WeatherServiceTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 42d7eaf..8f50f01 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,8 @@ dependencies { implementation("com.vaadin:vaadin-core:24.+") implementation("com.vaadin:vaadin-spring-boot-starter:24.+") testImplementation(kotlin("test")) - testImplementation("io.mockk:mockk:1.4.1") + testImplementation("io.mockk:mockk:1.9.3") + testImplementation("org.springframework.boot:spring-boot-starter-test") } defaultTasks("clean", "vaadinBuildFrontend", "build") diff --git a/app/src/main/kotlin/WeatherCommandLineRunner.kt b/app/src/main/kotlin/WeatherCommandLineRunner.kt index 40361ab..c5dffa4 100644 --- a/app/src/main/kotlin/WeatherCommandLineRunner.kt +++ b/app/src/main/kotlin/WeatherCommandLineRunner.kt @@ -26,4 +26,4 @@ class WeatherCommandLineRunner(val weatherService: WeatherService): CommandLineR fun WeatherForecasts.println() = "Weather forecast daily averages:\n$separator\n${this.weatherDailyForecast .map { wdf -> "${wdf.day} | ${wdf.average()}"} - .reduce { first, second -> "${first}\n${second}"}}" \ No newline at end of file + .reduce { first, second -> "${first}\n${second}"}}\n" \ No newline at end of file diff --git a/app/src/test/kotlin/WeatherAppTest.kt b/app/src/test/kotlin/WeatherAppTest.kt deleted file mode 100644 index a81a55a..0000000 --- a/app/src/test/kotlin/WeatherAppTest.kt +++ /dev/null @@ -1,11 +0,0 @@ -package hu.vanio.kotlin.feladat.ms - -import kotlin.test.Test - -class WeatherAppTest { - - @Test fun `sikeres lekerdezes`() { - TODO() - } - -} \ No newline at end of file diff --git a/app/src/test/kotlin/WeatherCommandLineRunnerTest.kt b/app/src/test/kotlin/WeatherCommandLineRunnerTest.kt new file mode 100644 index 0000000..4e5a6d9 --- /dev/null +++ b/app/src/test/kotlin/WeatherCommandLineRunnerTest.kt @@ -0,0 +1,61 @@ + +import hu.vanio.kotlin.feladat.ms.WeatherCommandLineRunner +import hu.vanio.kotlin.feladat.ms.exception.ServiceUnavailable +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherDailyForecast +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecasts +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.time.LocalDate +import java.time.Month + +class WeatherCommandLineRunnerTest { + @MockK + lateinit var weatherService: WeatherService + + @BeforeEach + fun setUp() = MockKAnnotations.init(this) + + @Test + fun testConsoleLog() { + every { weatherService.getWeatherStatistics() } returns WeatherForecasts(listOf( + WeatherDailyForecast(LocalDate.of(2024, Month.MARCH, 1), listOf(1.0, 2.0, 3.0)))) + val weatherCommandLineRunner = WeatherCommandLineRunner(weatherService) + + assertStandardOutput("----------------------------------------------\n" + + "Weather forecast daily averages:\n" + + "----------------------------------------------\n" + + "2024-03-01 | 2.0\n" + + "----------------------------------------------") { weatherCommandLineRunner.run() } + } + + @Test + fun testServiceUnavailable() { + every { weatherService.getWeatherStatistics() } throws ServiceUnavailable("") + val weatherCommandLineRunner = WeatherCommandLineRunner(weatherService) + + assertStandardOutput("Weather forecast service is unavailable") { weatherCommandLineRunner.run() } + } + + private fun assertStandardOutput(expectedStdOut: String, functionCall: Runnable) { + val outputStream = ByteArrayOutputStream() + val printStream = PrintStream(outputStream) + val originalOut = System.out + + try { + System.setOut(printStream) + + functionCall.run() + + assertEquals(expectedStdOut, outputStream.toString().trim()) + } finally { + System.setOut(originalOut) + } + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt b/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt deleted file mode 100644 index d23ea17..0000000 --- a/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt +++ /dev/null @@ -1,394 +0,0 @@ -package openmeteo - -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule -import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecast -import org.junit.jupiter.api.Test -import java.time.LocalDate -import java.time.format.DateTimeFormatter - -class WeatherMeanTimeServiceTest() { - private val objectMapper = ObjectMapper().also { - it.registerModules(JavaTimeModule(), ParameterNamesModule()) - it.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false) - } - - val response = """ - { - "latitude": 47.5, - "longitude": 19.0625, - "generationtime_ms": 0.030040740966796875, - "utc_offset_seconds": 3600, - "timezone": "Europe/Budapest", - "timezone_abbreviation": "CET", - "elevation": 124, - "hourly_units": { - "time": "iso8601", - "temperature_2m": "°C" - }, - "hourly": { - "time": [ - "2024-03-18T00:00", - "2024-03-18T01:00", - "2024-03-18T02:00", - "2024-03-18T03:00", - "2024-03-18T04:00", - "2024-03-18T05:00", - "2024-03-18T06:00", - "2024-03-18T07:00", - "2024-03-18T08:00", - "2024-03-18T09:00", - "2024-03-18T10:00", - "2024-03-18T11:00", - "2024-03-18T12:00", - "2024-03-18T13:00", - "2024-03-18T14:00", - "2024-03-18T15:00", - "2024-03-18T16:00", - "2024-03-18T17:00", - "2024-03-18T18:00", - "2024-03-18T19:00", - "2024-03-18T20:00", - "2024-03-18T21:00", - "2024-03-18T22:00", - "2024-03-18T23:00", - "2024-03-19T00:00", - "2024-03-19T01:00", - "2024-03-19T02:00", - "2024-03-19T03:00", - "2024-03-19T04:00", - "2024-03-19T05:00", - "2024-03-19T06:00", - "2024-03-19T07:00", - "2024-03-19T08:00", - "2024-03-19T09:00", - "2024-03-19T10:00", - "2024-03-19T11:00", - "2024-03-19T12:00", - "2024-03-19T13:00", - "2024-03-19T14:00", - "2024-03-19T15:00", - "2024-03-19T16:00", - "2024-03-19T17:00", - "2024-03-19T18:00", - "2024-03-19T19:00", - "2024-03-19T20:00", - "2024-03-19T21:00", - "2024-03-19T22:00", - "2024-03-19T23:00", - "2024-03-20T00:00", - "2024-03-20T01:00", - "2024-03-20T02:00", - "2024-03-20T03:00", - "2024-03-20T04:00", - "2024-03-20T05:00", - "2024-03-20T06:00", - "2024-03-20T07:00", - "2024-03-20T08:00", - "2024-03-20T09:00", - "2024-03-20T10:00", - "2024-03-20T11:00", - "2024-03-20T12:00", - "2024-03-20T13:00", - "2024-03-20T14:00", - "2024-03-20T15:00", - "2024-03-20T16:00", - "2024-03-20T17:00", - "2024-03-20T18:00", - "2024-03-20T19:00", - "2024-03-20T20:00", - "2024-03-20T21:00", - "2024-03-20T22:00", - "2024-03-20T23:00", - "2024-03-21T00:00", - "2024-03-21T01:00", - "2024-03-21T02:00", - "2024-03-21T03:00", - "2024-03-21T04:00", - "2024-03-21T05:00", - "2024-03-21T06:00", - "2024-03-21T07:00", - "2024-03-21T08:00", - "2024-03-21T09:00", - "2024-03-21T10:00", - "2024-03-21T11:00", - "2024-03-21T12:00", - "2024-03-21T13:00", - "2024-03-21T14:00", - "2024-03-21T15:00", - "2024-03-21T16:00", - "2024-03-21T17:00", - "2024-03-21T18:00", - "2024-03-21T19:00", - "2024-03-21T20:00", - "2024-03-21T21:00", - "2024-03-21T22:00", - "2024-03-21T23:00", - "2024-03-22T00:00", - "2024-03-22T01:00", - "2024-03-22T02:00", - "2024-03-22T03:00", - "2024-03-22T04:00", - "2024-03-22T05:00", - "2024-03-22T06:00", - "2024-03-22T07:00", - "2024-03-22T08:00", - "2024-03-22T09:00", - "2024-03-22T10:00", - "2024-03-22T11:00", - "2024-03-22T12:00", - "2024-03-22T13:00", - "2024-03-22T14:00", - "2024-03-22T15:00", - "2024-03-22T16:00", - "2024-03-22T17:00", - "2024-03-22T18:00", - "2024-03-22T19:00", - "2024-03-22T20:00", - "2024-03-22T21:00", - "2024-03-22T22:00", - "2024-03-22T23:00", - "2024-03-23T00:00", - "2024-03-23T01:00", - "2024-03-23T02:00", - "2024-03-23T03:00", - "2024-03-23T04:00", - "2024-03-23T05:00", - "2024-03-23T06:00", - "2024-03-23T07:00", - "2024-03-23T08:00", - "2024-03-23T09:00", - "2024-03-23T10:00", - "2024-03-23T11:00", - "2024-03-23T12:00", - "2024-03-23T13:00", - "2024-03-23T14:00", - "2024-03-23T15:00", - "2024-03-23T16:00", - "2024-03-23T17:00", - "2024-03-23T18:00", - "2024-03-23T19:00", - "2024-03-23T20:00", - "2024-03-23T21:00", - "2024-03-23T22:00", - "2024-03-23T23:00", - "2024-03-24T00:00", - "2024-03-24T01:00", - "2024-03-24T02:00", - "2024-03-24T03:00", - "2024-03-24T04:00", - "2024-03-24T05:00", - "2024-03-24T06:00", - "2024-03-24T07:00", - "2024-03-24T08:00", - "2024-03-24T09:00", - "2024-03-24T10:00", - "2024-03-24T11:00", - "2024-03-24T12:00", - "2024-03-24T13:00", - "2024-03-24T14:00", - "2024-03-24T15:00", - "2024-03-24T16:00", - "2024-03-24T17:00", - "2024-03-24T18:00", - "2024-03-24T19:00", - "2024-03-24T20:00", - "2024-03-24T21:00", - "2024-03-24T22:00", - "2024-03-24T23:00" - ], - "temperature_2m": [ - 6, - 5.7, - 5.4, - 5.2, - 4.8, - 4.6, - 4.5, - 4.7, - 5.9, - 7.3, - 8.9, - 9.7, - 10.4, - 10.9, - 11.1, - 11, - 10.6, - 9.8, - 8.7, - 7.4, - 6.2, - 5.4, - 4.7, - 4.2, - 3.7, - 3.3, - 3, - 2.9, - 2.8, - 2.8, - 2.8, - 3.2, - 3.9, - 4.7, - 5.5, - 6.4, - 7.4, - 8, - 8.5, - 8.8, - 8.8, - 8.6, - 7.8, - 6.7, - 5.6, - 4.7, - 4, - 3.4, - 2.8, - 2.3, - 1.9, - 1.6, - 1.3, - 1, - 0.8, - 1.1, - 3.5, - 6.8, - 9.1, - 10.5, - 11.5, - 12.2, - 12.8, - 13.1, - 12.9, - 12.3, - 10.9, - 9.1, - 7.8, - 6.9, - 6, - 5.4, - 4.6, - 4.3, - 4.1, - 4.1, - 4.2, - 4.3, - 4.1, - 4.1, - 6.1, - 8.3, - 10.1, - 11.5, - 12.6, - 13.3, - 13.9, - 14.2, - 14.1, - 13.4, - 12.4, - 11.4, - 10.7, - 10.1, - 9.5, - 9.1, - 8.8, - 8.5, - 8.2, - 8, - 7.8, - 7.4, - 7, - 7.2, - 8.2, - 9.6, - 11.2, - 12.7, - 14.3, - 15.5, - 16.2, - 16.4, - 16.1, - 15.3, - 14, - 12.7, - 11.7, - 10.7, - 9.9, - 9.4, - 9.1, - 8.9, - 8.7, - 8.5, - 8.3, - 8.1, - 7.9, - 7.7, - 10.8, - 12.7, - 14.5, - 16.4, - 18.3, - 19.7, - 20.4, - 20.6, - 20.3, - 19.5, - 18.2, - 16.7, - 14.8, - 12.7, - 10.9, - 9.9, - 9.3, - 8.7, - 8.2, - 7.8, - 7.5, - 7.2, - 7, - 6.9, - 7.1, - 7.5, - 7.8, - 7.8, - 7.6, - 7.5, - 7.4, - 7.4, - 7.4, - 7.2, - 6.9, - 6.8, - 6.8, - 7, - 7.1, - 7.1 - ] - } -} - """.trimIndent() - - @Test - fun test() { - val weatherForecast = objectMapper.readValue(response, WeatherForecast::class.java) - val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm") - val weatherMap = mutableMapOf<LocalDate, MutableList<Double>>() - - var i = 0 - weatherForecast.hourly.time.forEach { - val temperature = weatherForecast.hourly.temperature2m[i++] - val key = LocalDate.parse(it, dateTimeFormatter) - weatherMap.compute(key) { _, value -> - value?.plus(temperature)?.toMutableList() ?: mutableListOf(temperature) - } - } - weatherMap.forEach { - println("${it.key} - ${it.value.average()}") - } - } -} \ No newline at end of file diff --git a/app/src/test/kotlin/openmeteo/WeatherServiceTest.kt b/app/src/test/kotlin/openmeteo/WeatherServiceTest.kt new file mode 100644 index 0000000..da16702 --- /dev/null +++ b/app/src/test/kotlin/openmeteo/WeatherServiceTest.kt @@ -0,0 +1,133 @@ +package openmeteo + +import hu.vanio.kotlin.feladat.ms.openmeteo.Hourly +import hu.vanio.kotlin.feladat.ms.openmeteo.HourlyUnits +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecast +import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import java.time.LocalDate +import java.time.Month +import kotlin.test.assertEquals + +class WeatherServiceTest { + private val weatherForecastResponse = WeatherForecast(47.5, + 19.0625, + 0.030040740966796875, + 3600, + "Europe/Budapest", + "CET", + 124.0, + hourlyUnits = HourlyUnits("iso8601", "°C"), + hourly = Hourly(time = listOf("2024-03-18T00:00", + "2024-03-18T01:00", + "2024-03-18T02:00", + "2024-03-18T03:00", + "2024-03-18T04:00", + "2024-03-18T05:00", + "2024-03-18T06:00", + "2024-03-18T07:00", + "2024-03-18T08:00", + "2024-03-18T09:00", + "2024-03-18T10:00", + "2024-03-18T11:00", + "2024-03-18T12:00", + "2024-03-18T13:00", + "2024-03-18T14:00", + "2024-03-18T15:00", + "2024-03-18T16:00", + "2024-03-18T17:00", + "2024-03-18T18:00", + "2024-03-18T19:00", + "2024-03-18T20:00", + "2024-03-18T21:00", + "2024-03-18T22:00", + "2024-03-18T23:00", + "2024-03-19T00:00", + "2024-03-19T01:00", + "2024-03-19T02:00", + "2024-03-19T03:00", + "2024-03-19T04:00", + "2024-03-19T05:00", + "2024-03-19T06:00", + "2024-03-19T07:00", + "2024-03-19T08:00", + "2024-03-19T09:00", + "2024-03-19T10:00", + "2024-03-19T11:00", + "2024-03-19T12:00", + "2024-03-19T13:00", + "2024-03-19T14:00", + "2024-03-19T15:00", + "2024-03-19T16:00", + "2024-03-19T17:00", + "2024-03-19T18:00", + "2024-03-19T19:00", + "2024-03-19T20:00", + "2024-03-19T21:00", + "2024-03-19T22:00", + "2024-03-19T23:00"), + temperature2m = listOf(6.0, + 5.7, + 5.4, + 5.2, + 4.8, + 4.6, + 4.5, + 4.7, + 5.9, + 7.3, + 8.9, + 9.7, + 10.4, + 10.9, + 11.1, + 11.0, + 10.6, + 9.8, + 8.7, + 7.4, + 6.2, + 5.4, + 4.7, + 4.2, + 3.7, + 3.3, + 3.0, + 2.9, + 2.8, + 2.8, + 2.8, + 3.2, + 3.9, + 4.7, + 5.5, + 6.4, + 7.4, + 8.0, + 8.5, + 8.8, + 8.8, + 8.6, + 7.8, + 6.7, + 5.6, + 4.7, + 4.0, + 3.4)) + ) + + @Test + fun statistics() { + val weatherForecasts = WeatherService.statistics(weatherForecastResponse) + + Assertions.assertAll ( + Executable { assertEquals(2, weatherForecasts.weatherDailyForecast.size) } , + Executable { assertEquals(LocalDate.of(2024, Month.MARCH, 18), weatherForecasts.weatherDailyForecast[0].day) }, + Executable { assertEquals(7.21, weatherForecasts.weatherDailyForecast[0].average()) }, + Executable { assertEquals(LocalDate.of(2024, Month.MARCH, 19), weatherForecasts.weatherDailyForecast[1].day) }, + Executable { assertEquals(5.3, weatherForecasts.weatherDailyForecast[1].average()) } + ) + } +} \ No newline at end of file