Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wittner #5

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
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")
testImplementation("io.mockk:mockk:1.9.3")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}

defaultTasks("clean", "vaadinBuildFrontend", "build")

tasks.test {
useJUnitPlatform()
}

kotlin {
jvmToolchain(17)
}

vaadin {
optimizeBundle = false
}
7 changes: 6 additions & 1 deletion app/src/main/kotlin/WeatherApp.kt
Original file line number Diff line number Diff line change
@@ -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>()
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/kotlin/WeatherAppConfig.kt
Original file line number Diff line number Diff line change
@@ -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 = "")
29 changes: 29 additions & 0 deletions app/src/main/kotlin/WeatherCommandLineRunner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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
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?) {
val weatherStatistics = try { weatherService.getWeatherStatistics() } catch (e: ServiceUnavailable) {
println("Weather forecast service is unavailable")
return
}
println("$separator\n${weatherStatistics.println()}$separator")
}
}

fun WeatherForecasts.println() =
"Weather forecast daily averages:\n$separator\n${this.weatherDailyForecast
.map { wdf -> "${wdf.day} | ${wdf.average()}"}
.reduce { first, second -> "${first}\n${second}"}}\n"
6 changes: 6 additions & 0 deletions app/src/main/kotlin/exception/ServiceUnavailable.kt
Original file line number Diff line number Diff line change
@@ -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"
}
31 changes: 31 additions & 0 deletions app/src/main/kotlin/openmeteo/WeatherForecast.kt
Original file line number Diff line number Diff line change
@@ -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>,
)
18 changes: 18 additions & 0 deletions app/src/main/kotlin/openmeteo/WeatherForecasts.kt
Original file line number Diff line number Diff line change
@@ -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>)
51 changes: 51 additions & 0 deletions app/src/main/kotlin/openmeteo/WeatherService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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
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) {
private val id = "WEATHER_SERVICE"

fun getWeatherStatistics(): WeatherForecasts {
val request = HttpRequest.newBuilder()
.uri(URI(config.appUrl))
.GET()
.build()
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)
}

companion object {
fun statistics(weatherForecast: WeatherForecast): WeatherForecasts {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ez a függvény miért lett companion object?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mehetett volna külön service-be is (talán a név sem túl jó, tulajdonképpen ez csak egy mapper).

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())
}
}
}
18 changes: 18 additions & 0 deletions app/src/main/kotlin/vaadin/CustomErrorHandler.kt
Original file line number Diff line number Diff line change
@@ -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})")
}
}
}
}
38 changes: 38 additions & 0 deletions app/src/main/kotlin/vaadin/MainView.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
13 changes: 13 additions & 0 deletions app/src/main/kotlin/vaadin/VaadinInitListener.kt
Original file line number Diff line number Diff line change
@@ -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}
}
}
12 changes: 12 additions & 0 deletions app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -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
11 changes: 0 additions & 11 deletions app/src/test/kotlin/WeatherAppTest.kt

This file was deleted.

61 changes: 61 additions & 0 deletions app/src/test/kotlin/WeatherCommandLineRunnerTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading