Finished migration to Kotlin

This commit is contained in:
2023-07-30 18:17:40 +02:00
parent 13ac42d875
commit 910108f88d
54 changed files with 1325 additions and 1687 deletions
@@ -1,5 +1,5 @@
import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.accessors.dm.LibrariesForLibs
import java.time.LocalDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
plugins { plugins {
@@ -51,7 +51,7 @@ tasks.jacocoTestReport {
} }
} }
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
tasks.jar { tasks.jar {
doFirst { doFirst {
@@ -59,7 +59,7 @@ tasks.jar {
attributes["Implementation-Title"] = rootProject.name attributes["Implementation-Title"] = rootProject.name
attributes["Implementation-Version"] = archiveVersion.get() attributes["Implementation-Version"] = archiveVersion.get()
attributes["Implementation-Vendor"] = "Jim Martens" attributes["Implementation-Vendor"] = "Jim Martens"
attributes["Build-Timestamp"] = LocalDateTime.now().format(formatter) attributes["Build-Timestamp"] = ZonedDateTime.now().format(formatter)
attributes["Created-By"] = "Gradle ${gradle.gradleVersion}" attributes["Created-By"] = "Gradle ${gradle.gradleVersion}"
attributes["Build-Jdk"] = "${providers.systemProperty("java.version").get()} (${providers.systemProperty("java.vendor").get()} ${providers.systemProperty("java.vm.version").get()})" attributes["Build-Jdk"] = "${providers.systemProperty("java.version").get()} (${providers.systemProperty("java.vendor").get()} ${providers.systemProperty("java.vm.version").get()})"
attributes["Build-OS"] = "${providers.systemProperty("os.name").get()} ${providers.systemProperty("os.arch").get()} ${providers.systemProperty("os.version").get()}" attributes["Build-OS"] = "${providers.systemProperty("os.name").get()} ${providers.systemProperty("os.arch").get()} ${providers.systemProperty("os.version").get()}"
@@ -12,7 +12,6 @@ dependencies {
implementation(libs.bundles.spring.boot) implementation(libs.bundles.spring.boot)
testImplementation(libs.spring.boot.test) testImplementation(libs.spring.boot.test)
annotationProcessor(libs.spring.boot.config)
} }
sourceSets { sourceSets {
@@ -31,7 +30,7 @@ idea {
} }
} }
val integrationTestImplementation by configurations.getting { val integrationTestImplementation: Configuration by configurations.getting {
extendsFrom(configurations.testImplementation.get()) extendsFrom(configurations.testImplementation.get())
} }
@@ -70,3 +69,15 @@ tasks.jar {
enabled = true enabled = true
archiveClassifier.set("") archiveClassifier.set("")
} }
tasks.bootDistZip {
dependsOn(tasks.jar)
}
tasks.bootDistTar {
dependsOn(tasks.jar)
}
tasks.bootStartScripts {
dependsOn(tasks.jar)
}
+1
View File
@@ -7,3 +7,4 @@ org.gradle.daemon=true
org.gradle.welcome=never org.gradle.welcome=never
org.gradle.caching=true org.gradle.caching=true
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
kapt.include.compile.classpath=false
-5
View File
@@ -2,7 +2,6 @@ plugins {
id("twomartens.spring-boot-cloud") id("twomartens.spring-boot-cloud")
id("twomartens.kotlin") id("twomartens.kotlin")
kotlin("kapt") kotlin("kapt")
alias(libs.plugins.kotlin.lombok)
} }
dependencies { dependencies {
@@ -11,7 +10,3 @@ dependencies {
annotationProcessor(libs.mapstruct.processor) annotationProcessor(libs.mapstruct.processor)
kapt(libs.mapstruct.processor) kapt(libs.mapstruct.processor)
} }
kapt {
keepJavacAnnotationProcessors = true
}
@@ -6,7 +6,7 @@ import org.springframework.context.annotation.Configuration
import java.time.Clock import java.time.Clock
@Configuration @Configuration
open class ClockConfiguration(val serviceProperties: ServiceProperties) { open class ClockConfiguration(private val serviceProperties: ServiceProperties) {
@Bean @Bean
open fun clock(): Clock { open fun clock(): Clock {
@@ -1,39 +1,39 @@
package de.twomartens.wahlrecht.configuration; package de.twomartens.wahlrecht.configuration
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.OAuthFlow; import io.swagger.v3.oas.annotations.security.OAuthFlow
import io.swagger.v3.oas.annotations.security.OAuthFlows; import io.swagger.v3.oas.annotations.security.OAuthFlows
import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI
import org.springframework.beans.factory.annotation.Value; import io.swagger.v3.oas.models.info.Info
import org.springframework.context.annotation.Bean; import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@SecurityScheme( @SecurityScheme(name = "bearerAuth", type = SecuritySchemeType.HTTP, scheme = "bearer", bearerFormat = "JWT")
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT"
)
@SecurityScheme( @SecurityScheme(
name = "oauth2", name = "oauth2",
type = SecuritySchemeType.OAUTH2, type = SecuritySchemeType.OAUTH2,
flows = @OAuthFlows( flows = OAuthFlows(
implicit = @OAuthFlow( implicit = OAuthFlow(
authorizationUrl = "https://id.2martens.de/realms/2martens/protocol/openid-connect/auth", authorizationUrl = "https://id.2martens.de/realms/2martens/protocol/openid-connect/auth",
tokenUrl = "https://id.2martens.de/realms/2martens/protocol/openid-connect/token" tokenUrl = "https://id.2martens.de/realms/2martens/protocol/openid-connect/token"
) )
) )
) )
@Configuration @Configuration
public class OpenApiConfiguration { open class OpenApiConfiguration {
@Bean @Bean
public OpenAPI customOpenAPI(@Value("${openapi.description}") String apiDesciption, open fun customOpenAPI(
@Value("${openapi.version}") String apiVersion, @Value("${openapi.title}") String apiTitle) { @Value("\${openapi.description}") apiDescription: String,
return new OpenAPI() @Value("\${openapi.version}") apiVersion: String, @Value("\${openapi.title}") apiTitle: String
.info(new io.swagger.v3.oas.models.info.Info() ): OpenAPI {
return OpenAPI()
.info(
Info()
.title(apiTitle) .title(apiTitle)
.version(apiVersion) .version(apiVersion)
.description(apiDesciption)); .description(apiDescription)
)
} }
} }
@@ -1,28 +1,25 @@
package de.twomartens.wahlrecht.configuration; package de.twomartens.wahlrecht.configuration
import de.twomartens.wahlrecht.monitoring.statusprobe.CountBasedStatusProbe; import de.twomartens.wahlrecht.monitoring.statusprobe.CountBasedStatusProbe
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbe; import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbe
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality; import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeLogger; import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeLogger
import java.time.Clock; import org.springframework.context.annotation.Bean
import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Bean; import java.time.Clock
import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@RequiredArgsConstructor open class StatusProbeConfiguration(private val clock: Clock) {
public class StatusProbeConfiguration {
private final Clock clock;
@Bean @Bean
public StatusProbeLogger statusProbeLogger() { open fun statusProbeLogger(): StatusProbeLogger {
return new StatusProbeLogger(clock); return StatusProbeLogger(clock)
} }
@Bean @Bean
public StatusProbe testStatusProbe(StatusProbeLogger statusProbeLogger) { open fun testStatusProbe(statusProbeLogger: StatusProbeLogger): StatusProbe {
return new CountBasedStatusProbe(1, return CountBasedStatusProbe(
clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger); 1,
clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger
)
} }
} }
@@ -1,33 +1,25 @@
package de.twomartens.wahlrecht.configuration; package de.twomartens.wahlrecht.configuration
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest; import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest
import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod
import org.springframework.http.HttpMethod; import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.lang.NonNull; import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.CorsRegistration; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
@RequiredArgsConstructor open class WebConfiguration(private val headerInterceptorRest: HeaderInterceptorRest) : WebMvcConfigurer {
public class WebConfiguration implements WebMvcConfigurer { override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(headerInterceptorRest)
private final HeaderInterceptorRest headerInterceptorRest;
@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
registry.addInterceptor(headerInterceptorRest);
} }
@Override override fun addCorsMappings(registry: CorsRegistry) {
public void addCorsMappings(@NonNull CorsRegistry registry) { val registration = registry.addMapping("/**")
CorsRegistration registration = registry.addMapping("/**"); registration.allowedMethods(
registration.allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.GET.name(), HttpMethod.POST.name(),
HttpMethod.PUT.name(), HttpMethod.OPTIONS.name()); HttpMethod.PUT.name(), HttpMethod.OPTIONS.name()
registration.allowCredentials(true); )
registration.allowedOrigins("http://localhost:4200"); registration.allowCredentials(true)
registration.allowedOrigins("http://localhost:4200")
} }
} }
@@ -1,58 +1,41 @@
package de.twomartens.wahlrecht.configuration; package de.twomartens.wahlrecht.configuration
import de.twomartens.wahlrecht.security.SpringPolicyEnforcerFilter
import de.twomartens.wahlrecht.security.SpringPolicyEnforcerFilter; import org.keycloak.adapters.authorization.spi.ConfigurationResolver
import java.io.IOException; import org.keycloak.adapters.authorization.spi.HttpRequest
import java.util.ArrayList; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
import java.util.Collection; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode
import java.util.List; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig
import java.util.Map; import org.keycloak.util.JsonSerialization
import org.keycloak.adapters.authorization.spi.ConfigurationResolver; import org.springframework.beans.factory.annotation.Value
import org.keycloak.adapters.authorization.spi.HttpRequest; import org.springframework.context.annotation.Bean
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import org.springframework.context.annotation.Configuration
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode; import org.springframework.http.HttpMethod
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.keycloak.util.JsonSerialization; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.beans.factory.annotation.Value; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
import org.springframework.context.annotation.Bean; import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import org.springframework.http.HttpMethod; import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter
import org.springframework.lang.NonNull; import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import java.io.IOException
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
public class WebSecurityConfiguration { open class WebSecurityConfiguration {
@Value("\${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private static final Collection<String> PERMITTED_PATHS = List.of( private lateinit var jwkSetUri: String
"/wahlrecht/v1/healthCheck",
"/actuator/**",
"/wahlrecht/v1/doc/**",
"/wahlrecht/v1/api-docs/**",
"/error");
private static final List<PathConfig> PATHS = buildPathConfigs();
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkSetUri;
@Value("#{environment.CLIENT_SECRET}") @Value("#{environment.CLIENT_SECRET}")
private String clientSecret; private lateinit var clientSecret: String
@Bean @Bean
public SecurityFilterChain securityFilterChain(@NonNull HttpSecurity http) throws Exception { @Throws(Exception::class)
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http http
.csrf().disable() .csrf().disable()
.authorizeHttpRequests() .authorizeHttpRequests()
.requestMatchers(PERMITTED_PATHS.toArray(new String[0])) .requestMatchers(*PERMITTED_PATHS.toTypedArray<String>())
.permitAll() .permitAll()
.and() .and()
.authorizeHttpRequests() .authorizeHttpRequests()
@@ -63,50 +46,57 @@ public class WebSecurityConfiguration {
.anyRequest() .anyRequest()
.authenticated() .authenticated()
.and() .and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) .oauth2ResourceServer { obj: OAuth2ResourceServerConfigurer<HttpSecurity?> -> obj.jwt() }
.addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter.class); .addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter::class.java)
return http.build(); return http.build()
} }
@Bean @Bean
JwtDecoder jwtDecoder() { open fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build(); return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
} }
private SpringPolicyEnforcerFilter createPolicyEnforcerFilter() { private fun createPolicyEnforcerFilter(): SpringPolicyEnforcerFilter {
return new SpringPolicyEnforcerFilter(new ConfigurationResolver() { return SpringPolicyEnforcerFilter(object : ConfigurationResolver {
@Override override fun resolve(request: HttpRequest): PolicyEnforcerConfig {
public PolicyEnforcerConfig resolve(HttpRequest request) { return try {
try { val policyEnforcerConfig = JsonSerialization.readValue(
PolicyEnforcerConfig policyEnforcerConfig = JsonSerialization.readValue( javaClass.getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig::class.java
getClass().getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig.class); )
policyEnforcerConfig.setCredentials(Map.of("secret", clientSecret)); policyEnforcerConfig.credentials = mapOf(Pair("secret", clientSecret))
if (request.getMethod().equals(HttpMethod.OPTIONS.name())) { if (request.method == HttpMethod.OPTIONS.name()) {
// always allow options request // always allow options request
policyEnforcerConfig.setEnforcementMode(EnforcementMode.DISABLED); policyEnforcerConfig.enforcementMode = EnforcementMode.DISABLED
} else { } else {
policyEnforcerConfig.setPaths(PATHS); policyEnforcerConfig.paths = PATHS
}
policyEnforcerConfig
} catch (e: IOException) {
throw RuntimeException(e)
}
}
})
} }
return policyEnforcerConfig; companion object {
} catch (IOException e) { private val PERMITTED_PATHS: Collection<String> = listOf(
throw new RuntimeException(e); "/wahlrecht/v1/healthCheck",
} "/actuator/**",
} "/wahlrecht/v1/doc/**",
}); "/wahlrecht/v1/api-docs/**",
} "/error"
)
private val PATHS = buildPathConfigs()
@NonNull private fun buildPathConfigs(): List<PathConfig> {
private static List<PathConfig> buildPathConfigs() { val paths: MutableList<PathConfig> = mutableListOf()
List<PathConfig> paths = new ArrayList<>(); for (path in PERMITTED_PATHS) {
for (String path : PERMITTED_PATHS) { val pathConfig = PathConfig()
PathConfig pathConfig = new PathConfig(); pathConfig.path = path.replace("**", "*")
pathConfig.setPath(path.replace("**", "*")); pathConfig.enforcementMode = EnforcementMode.DISABLED
pathConfig.setEnforcementMode(EnforcementMode.DISABLED); paths.add(pathConfig)
paths.add(pathConfig); }
return paths
} }
return paths;
} }
} }
@@ -1,61 +1,58 @@
package de.twomartens.wahlrecht.controller.v1; package de.twomartens.wahlrecht.controller.v1
import de.twomartens.wahlrecht.mapper.v1.ElectedCandidatesMapper; import de.twomartens.wahlrecht.mapper.v1.ElectedCandidatesMapper
import de.twomartens.wahlrecht.mapper.v1.ElectionResultMapper; import de.twomartens.wahlrecht.mapper.v1.ElectionResultMapper
import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidates; import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidates
import de.twomartens.wahlrecht.model.dto.v1.ElectionResult; import de.twomartens.wahlrecht.model.dto.v1.ElectionResult
import de.twomartens.wahlrecht.service.CalculationService; import de.twomartens.wahlrecht.service.CalculationService
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag
import lombok.RequiredArgsConstructor; import org.mapstruct.factory.Mappers
import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType
import org.mapstruct.factory.Mappers; import org.springframework.http.ResponseEntity
import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping
import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController @RestController
@RequiredArgsConstructor @RequestMapping(value = ["/wahlrecht/v1"])
@RequestMapping(value = "/wahlrecht/v1")
@Tag(name = "Calculations", description = "all requests relating to calculations") @Tag(name = "Calculations", description = "all requests relating to calculations")
public class CalculationController { class CalculationController(private val service: CalculationService) {
private val electedCandidatesMapper = Mappers.getMapper(
private final ElectedCandidatesMapper electedCandidatesMapper = Mappers.getMapper( ElectedCandidatesMapper::class.java
ElectedCandidatesMapper.class); )
private final ElectionResultMapper electionResultMapper = Mappers.getMapper( private val electionResultMapper = Mappers.getMapper(
ElectionResultMapper.class); ElectionResultMapper::class.java
private final CalculationService service; )
@Operation( @Operation(
summary = "Calculates a provided election result", summary = "Calculates a provided election result",
description = "This request does not store any result and is idempotent.", description = "This request does not store any result and is idempotent.",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "200",
description = "Returns all elected candidates", description = "Returns all elected candidates",
content = {@Content( content = [Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = ElectedCandidates.class))} schema = Schema(implementation = ElectedCandidates::class)
)]
)]
) )
} @PostMapping(value = ["/calculate"], produces = [MediaType.APPLICATION_JSON_VALUE])
)
@PostMapping(value = "/calculate", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
public ResponseEntity<ElectedCandidates> calculateResult( fun calculateResult(
@RequestBody ElectionResult electionResult @RequestBody electionResult: ElectionResult
) { ): ResponseEntity<ElectedCandidates> {
ElectedCandidates result = electedCandidatesMapper.mapToExternal( val result = electedCandidatesMapper.mapToExternal(
service.determineElectedCandidates(electionResultMapper.mapToInternal(electionResult))); service.determineElectedCandidates(electionResultMapper.mapToInternal(electionResult))
)
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(result); .body(result)
} }
} }
@@ -1,105 +1,91 @@
package de.twomartens.wahlrecht.controller.v1; package de.twomartens.wahlrecht.controller.v1
import de.twomartens.wahlrecht.mapper.v1.ElectionMapper; import de.twomartens.wahlrecht.mapper.v1.ElectionMapper
import de.twomartens.wahlrecht.model.dto.v1.Election; import de.twomartens.wahlrecht.model.dto.v1.Election
import de.twomartens.wahlrecht.service.ElectionService; import de.twomartens.wahlrecht.service.ElectionService
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag
import java.util.Collection; import org.mapstruct.factory.Mappers
import java.util.List; import org.springframework.http.HttpStatus
import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType
import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity
import org.mapstruct.factory.Mappers; import org.springframework.web.bind.annotation.*
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController @RestController
@RequiredArgsConstructor @RequestMapping(value = ["/wahlrecht/v1"])
@RequestMapping(value = "/wahlrecht/v1")
@Tag(name = "Elections", description = "all requests relating to elections") @Tag(name = "Elections", description = "all requests relating to elections")
public class ElectionController { class ElectionController(private val service: ElectionService) {
private val mapper = Mappers.getMapper(ElectionMapper::class.java)
private final ElectionMapper mapper = Mappers.getMapper(ElectionMapper.class);
private final ElectionService service;
@SecurityRequirement(name = "oauth2")
@SecurityRequirement(name = "bearerAuth")
@GetMapping(value = ["/elections"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation( @Operation(
summary = "Returns all stored elections", summary = "Returns all stored elections",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "200",
description = "Returns all stored elections", description = "Returns all stored elections",
content = {@Content( content = [Content(
mediaType = "application/json", mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = Election.class)))} array = ArraySchema(
schema = Schema(
implementation = Election::class
) )
}
) )
@GetMapping(value = "/elections", produces = MediaType.APPLICATION_JSON_VALUE) )]
@SecurityRequirement(name = "bearerAuth") )]
@SecurityRequirement(name = "oauth2") )
public ResponseEntity<Collection<Election>> getElections() { fun getElections(): ResponseEntity<Collection<Election>> {
List<Election> elections = service.getElections().stream().map(mapper::mapToExternal).toList(); val elections = service.elections.asSequence()
.map { mapper.mapToExternal(it.value) }
.toList()
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(elections); .body(elections)
} }
@Operation( @Operation(
summary = "Returns election matching provided name", summary = "Returns election matching provided name",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "200",
description = "Returns matching election", description = "Returns matching election",
content = {@Content( content = [Content(mediaType = "application/json", schema = Schema(implementation = Election::class))]
mediaType = "application/json", )]
schema = @Schema(implementation = Election.class))}
) )
} @GetMapping(value = ["/elections/by-name/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
)
@GetMapping(value = "/elections/by-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
public ResponseEntity<Election> getElectionByName( fun getElectionByName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) { @PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String?
Election election = mapper.mapToExternal(service.getElection(electionName)); ): ResponseEntity<Election> {
val election = mapper.mapToExternal(service.elections[electionName]!!)
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(election); .body(election)
} }
@Operation( @Operation(
summary = "Stores a new election", summary = "Stores a new election",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "201", responseCode = "201",
description = "Election was created"), description = "Election was created"
@ApiResponse(responseCode = "200", ), ApiResponse(responseCode = "200", description = "Election was modified"), ApiResponse(
description = "Election was modified"), responseCode = "401",
@ApiResponse(responseCode = "401",
description = "Unauthorized", description = "Unauthorized",
content = {@Content( content = [Content(mediaType = "plain/text")]
mediaType = "plain/text")} )]
)
}
) )
@PutMapping("/election") @PutMapping("/election")
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
public ResponseEntity<?> putElection(@RequestBody Election election) { fun putElection(@RequestBody election: Election?): ResponseEntity<*> {
boolean createdNew = service.storeElection(mapper.mapToDB(election)); val createdNew = service.storeElection(mapper.mapToDB(election!!))
return createdNew return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK)
? new ResponseEntity<>(HttpStatus.CREATED)
: new ResponseEntity<>(HttpStatus.OK);
} }
} }
@@ -1,91 +1,75 @@
package de.twomartens.wahlrecht.controller.v1; package de.twomartens.wahlrecht.controller.v1
import de.twomartens.wahlrecht.mapper.v1.ElectionResultMapper; import de.twomartens.wahlrecht.mapper.v1.ElectionResultMapper
import de.twomartens.wahlrecht.model.dto.ErrorMessage; import de.twomartens.wahlrecht.model.dto.ErrorMessage
import de.twomartens.wahlrecht.model.dto.v1.ElectionResult; import de.twomartens.wahlrecht.model.dto.v1.ElectionResult
import de.twomartens.wahlrecht.service.ElectionResultService; import de.twomartens.wahlrecht.service.ElectionResultService
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus
import org.springframework.http.MediaType; import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping(value = "/wahlrecht/v1") @RequestMapping(value = ["/wahlrecht/v1"])
@Tag(name = "ElectionResults", description = "all requests relating to election results") @Tag(name = "ElectionResults", description = "all requests relating to election results")
public class ElectionResultController { class ElectionResultController(private val service: ElectionResultService) {
private val mapper = Mappers.getMapper(ElectionResultMapper::class.java)
private final ElectionResultMapper mapper = Mappers.getMapper(ElectionResultMapper.class);
private final ElectionResultService service;
@Operation( @Operation(
summary = "Returns election result for given election name", summary = "Returns election result for given election name",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "200",
description = "Returns stored election result for given election name", description = "Returns stored election result for given election name",
content = {@Content( content = [Content(mediaType = "application/json", schema = Schema(implementation = ElectionResult::class))]
mediaType = "application/json", ), ApiResponse(
schema = @Schema(implementation = ElectionResult.class))} responseCode = "404",
),
@ApiResponse(responseCode = "404",
description = "No election result found", description = "No election result found",
content = { content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorMessage::class))]
@Content( )]
mediaType = "application/json",
schema = @Schema(implementation = ErrorMessage.class)
) )
} @GetMapping(
value = ["/electionResult/by-election-name/{electionName}"],
produces = [MediaType.APPLICATION_JSON_VALUE]
) )
}
)
@GetMapping(value = "/electionResult/by-election-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
public ResponseEntity<ElectionResult> getElectionByName( fun getElectionByName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) { @PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String
ElectionResult result = mapper.mapToExternal(service.getElectionResult(electionName)); ): ResponseEntity<ElectionResult> {
val result = mapper.mapToExternal(service.getElectionResult(electionName))
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(result); .body(result)
} }
@Operation( @Operation(
summary = "Stores a new election", summary = "Stores a new election",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "201", responseCode = "201",
description = "Election was created"), description = "Election was created"
@ApiResponse(responseCode = "200", ), ApiResponse(responseCode = "200", description = "Election was modified"), ApiResponse(
description = "Election was modified"), responseCode = "401",
@ApiResponse(responseCode = "401",
description = "Unauthorized", description = "Unauthorized",
content = {@Content( content = [Content(mediaType = "plain/text")]
mediaType = "plain/text")} )]
)
}
) )
@PutMapping("/electionResult") @PutMapping("/electionResult")
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
public ResponseEntity<?> putElection(@RequestBody ElectionResult electionResult) { fun putElection(@RequestBody electionResult: ElectionResult?): ResponseEntity<*> {
boolean createdNew = service.storeResult(mapper.mapToDB(electionResult)); val createdNew = service.storeResult(mapper.mapToDB(electionResult!!))
return createdNew return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK)
? new ResponseEntity<>(HttpStatus.CREATED)
: new ResponseEntity<>(HttpStatus.OK);
} }
} }
@@ -1,95 +1,77 @@
package de.twomartens.wahlrecht.controller.v1; package de.twomartens.wahlrecht.controller.v1
import de.twomartens.wahlrecht.mapper.v1.PartyInElectionMapper; import de.twomartens.wahlrecht.mapper.v1.PartyInElectionMapper
import de.twomartens.wahlrecht.model.dto.ErrorMessage; import de.twomartens.wahlrecht.model.dto.ErrorMessage
import de.twomartens.wahlrecht.model.dto.v1.PartyInElection; import de.twomartens.wahlrecht.model.dto.v1.PartyInElection
import de.twomartens.wahlrecht.service.PartyService; import de.twomartens.wahlrecht.service.PartyService
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag
import java.util.Collection; import org.mapstruct.factory.Mappers
import java.util.List; import org.springframework.http.HttpStatus
import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType
import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity
import org.mapstruct.factory.Mappers; import org.springframework.stereotype.Controller
import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@Slf4j
@Controller @Controller
@RequiredArgsConstructor @RequestMapping(value = ["/wahlrecht/v1"])
@RequestMapping(value = "/wahlrecht/v1")
@Tag(name = "Parties", description = "all requests relating to parties") @Tag(name = "Parties", description = "all requests relating to parties")
public class PartyController { class PartyController(private val service: PartyService) {
private val mapper = Mappers.getMapper(PartyInElectionMapper::class.java)
private final PartyInElectionMapper mapper = Mappers.getMapper(PartyInElectionMapper.class);
private final PartyService service;
@Operation( @Operation(
summary = "Returns all stored parties for given election name", summary = "Returns all stored parties for given election name",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "200",
description = "Returns all stored parties", description = "Returns all stored parties",
content = {@Content( content = [Content(
mediaType = "application/json", mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = PartyInElection.class)))} array = ArraySchema(schema = Schema(implementation = PartyInElection::class))
), )]
@ApiResponse(responseCode = "404", ), ApiResponse(
responseCode = "404",
description = "No parties found", description = "No parties found",
content = { content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorMessage::class))]
@Content( )]
mediaType = "application/json",
schema = @Schema(implementation = ErrorMessage.class)
) )
} @GetMapping(value = ["/parties/by-election-name/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
)
}
)
@GetMapping(value = "/parties/by-election-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
public ResponseEntity<Collection<PartyInElection>> getPartiesByElectionName( fun getPartiesByElectionName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) { @PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String
List<PartyInElection> parties = service.getPartiesByElectionName(electionName).stream() ): ResponseEntity<Collection<PartyInElection>> {
.map(mapper::mapToExternal) val parties = service.getPartiesByElectionName(electionName).stream()
.toList(); .map { election: de.twomartens.wahlrecht.model.db.PartyInElection? ->
mapper.mapToExternal(
election!!
)
}
.toList()
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(parties); .body(parties)
} }
@Operation( @Operation(
summary = "Stores a new party in an election", summary = "Stores a new party in an election",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "201", responseCode = "201",
description = "Party was created"), description = "Party was created"
@ApiResponse(responseCode = "200", ), ApiResponse(responseCode = "200", description = "Party was modified"), ApiResponse(
description = "Party was modified"), responseCode = "401",
@ApiResponse(responseCode = "401",
description = "Unauthorized" description = "Unauthorized"
) )]
}
) )
@PutMapping("/party") @PutMapping("/party")
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
public ResponseEntity<?> putParty(@RequestBody PartyInElection party) { fun putParty(@RequestBody party: PartyInElection?): ResponseEntity<*> {
boolean createdNew = service.storeParty(mapper.mapToDB(party)); val createdNew = service.storeParty(mapper.mapToDB(party!!))
return createdNew return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK)
? new ResponseEntity<>(HttpStatus.CREATED)
: new ResponseEntity<>(HttpStatus.OK);
} }
} }
@@ -256,7 +256,7 @@ class LoggingInterceptorRest(
addLogString(stringMapMessage, PARAM_BUSINESS_TYPE, logMessage.businessType) addLogString(stringMapMessage, PARAM_BUSINESS_TYPE, logMessage.businessType)
addLogString(stringMapMessage, PARAM_REQUEST_BODY, cutToMaxLength(logMessage.requestBody)) addLogString(stringMapMessage, PARAM_REQUEST_BODY, cutToMaxLength(logMessage.requestBody))
addLogString(stringMapMessage, PARAM_RESPONSE_BODY, cutToMaxLength(logMessage.responseBody)) addLogString(stringMapMessage, PARAM_RESPONSE_BODY, cutToMaxLength(logMessage.responseBody))
log.debug(MARKER, stringMapMessage.toString(), logMessage.throwable) log.debug(MARKER, stringMapMessage.asString(), logMessage.throwable)
} }
private fun getDurationBetweenRequestAndResponseTime(logMessage: LogMessage): Duration { private fun getDurationBetweenRequestAndResponseTime(logMessage: LogMessage): Duration {
@@ -1,44 +1,24 @@
package de.twomartens.wahlrecht.model.db; package de.twomartens.wahlrecht.model.db
import java.time.Instant; import org.bson.types.ObjectId
import lombok.AccessLevel; import org.springframework.data.annotation.CreatedDate
import lombok.AllArgsConstructor; import org.springframework.data.annotation.Id
import lombok.Builder; import org.springframework.data.annotation.LastModifiedDate
import lombok.EqualsAndHashCode; import org.springframework.data.mongodb.core.index.Indexed
import lombok.Getter; import org.springframework.data.mongodb.core.mapping.Document
import lombok.NoArgsConstructor; import java.time.Instant
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
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;
@Document @Document
@Getter data class Candidate(
@Setter @Indexed var name: String,
@Builder var profession: String
@EqualsAndHashCode ){
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Candidate {
@EqualsAndHashCode.Exclude
@Id @Id
ObjectId id; var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
@Indexed @CreatedDate
String name; lateinit var created: Instant
String profession;
@LastModifiedDate
lateinit var lastModified: Instant
} }
@@ -1,46 +1,27 @@
package de.twomartens.wahlrecht.model.db; package de.twomartens.wahlrecht.model.db
import java.time.Instant; import org.bson.types.ObjectId
import lombok.AccessLevel; import org.springframework.data.annotation.CreatedDate
import lombok.AllArgsConstructor; import org.springframework.data.annotation.Id
import lombok.Builder; import org.springframework.data.annotation.LastModifiedDate
import lombok.EqualsAndHashCode; import org.springframework.data.mongodb.core.index.CompoundIndex
import lombok.Getter; import org.springframework.data.mongodb.core.mapping.Document
import lombok.NoArgsConstructor; import java.time.Instant
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
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;
@Document @Document
@Getter
@Setter
@Builder
@EqualsAndHashCode
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@CompoundIndex(def = "{'number': 1, 'electionName': 1}") @CompoundIndex(def = "{'number': 1, 'electionName': 1}")
public class Constituency { class Constituency(
var number: Int = 0,
@EqualsAndHashCode.Exclude var electionName: String,
var name: String,
var numberOfSeats: Int = 0
) {
@Id @Id
ObjectId id; var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
int number; @CreatedDate
String electionName; lateinit var created: Instant
String name;
int numberOfSeats; @LastModifiedDate
lateinit var lastModified: Instant
} }
@@ -1,50 +1,30 @@
package de.twomartens.wahlrecht.model.db; package de.twomartens.wahlrecht.model.db
import de.twomartens.wahlrecht.model.dto.v1.VotingThreshold; import de.twomartens.wahlrecht.model.dto.v1.VotingThreshold
import java.time.Instant; import org.bson.types.ObjectId
import java.time.LocalDate; import org.springframework.data.annotation.CreatedDate
import java.util.Collection; import org.springframework.data.annotation.Id
import lombok.AccessLevel; import org.springframework.data.annotation.LastModifiedDate
import lombok.AllArgsConstructor; import org.springframework.data.mongodb.core.index.Indexed
import lombok.Builder; import org.springframework.data.mongodb.core.mapping.Document
import lombok.EqualsAndHashCode; import java.time.Instant
import lombok.Getter; import java.time.LocalDate
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
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;
@Document @Document
@Getter data class Election(
@Setter @Indexed(unique = true) var name: String,
@Builder var day: LocalDate,
@EqualsAndHashCode var votingThreshold: VotingThreshold,
@ToString var totalNumberOfSeats: Int,
@FieldDefaults(level = AccessLevel.PRIVATE) var constituencies: Collection<Constituency>
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) ) {
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Election {
@EqualsAndHashCode.Exclude
@Id @Id
ObjectId id; var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude
@CreatedDate @CreatedDate
Instant created; lateinit var created: Instant
@EqualsAndHashCode.Exclude
@LastModifiedDate @LastModifiedDate
Instant lastModified; lateinit var lastModified: Instant
@Indexed(unique = true)
String name;
LocalDate day;
VotingThreshold votingThreshold;
int totalNumberOfSeats;
Collection<Constituency> constituencies;
} }
@@ -1,44 +1,24 @@
package de.twomartens.wahlrecht.model.db; package de.twomartens.wahlrecht.model.db
import java.time.Instant; import org.bson.types.ObjectId
import java.util.Collection; import org.springframework.data.annotation.CreatedDate
import java.util.Map; import org.springframework.data.annotation.Id
import lombok.AccessLevel; import org.springframework.data.annotation.LastModifiedDate
import lombok.AllArgsConstructor; import org.springframework.data.mongodb.core.mapping.Document
import lombok.Builder; import java.time.Instant
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.mongodb.core.mapping.Document;
@Document @Document
@Getter data class ElectionResult(
@Setter var electionName: String,
@Builder var overallResults: Collection<VotingResult>,
@EqualsAndHashCode var constituencyResults: Map<Int, Collection<VotingResult>>
@ToString ) {
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ElectionResult {
@EqualsAndHashCode.Exclude
@Id @Id
ObjectId id; var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
String electionName; @CreatedDate
Collection<VotingResult> overallResults; lateinit var created: Instant
Map<Integer, Collection<VotingResult>> constituencyResults;
@LastModifiedDate
lateinit var lastModified: Instant
} }
@@ -1,48 +1,31 @@
package de.twomartens.wahlrecht.model.db; package de.twomartens.wahlrecht.model.db
import java.time.Instant; import lombok.EqualsAndHashCode
import java.util.Map; import org.bson.types.ObjectId
import lombok.AccessLevel; import org.springframework.data.annotation.CreatedDate
import lombok.AllArgsConstructor; import org.springframework.data.annotation.Id
import lombok.Builder; import org.springframework.data.annotation.LastModifiedDate
import lombok.EqualsAndHashCode; import org.springframework.data.mongodb.core.index.CompoundIndex
import lombok.Getter; import org.springframework.data.mongodb.core.mapping.Document
import lombok.NoArgsConstructor; import java.time.Instant
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
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;
@Document @Document
@Getter
@Setter
@Builder
@EqualsAndHashCode
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@CompoundIndex(def = "{'abbreviation': 1, 'electionName': 1}") @CompoundIndex(def = "{'abbreviation': 1, 'electionName': 1}")
public class PartyInElection { data class PartyInElection(
var abbreviation: String,
@EqualsAndHashCode.Exclude var electionName: String,
var name: String,
var overallNomination: Nomination,
var constituencyNominations: Map<Int, Nomination?>
) {
@Id @Id
ObjectId id; var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude @EqualsAndHashCode.Exclude
@CreatedDate @CreatedDate
Instant created; lateinit var created: Instant
@EqualsAndHashCode.Exclude @EqualsAndHashCode.Exclude
@LastModifiedDate @LastModifiedDate
Instant lastModified; lateinit var lastModified: Instant
String abbreviation;
String electionName;
String name;
Nomination overallNomination;
Map<Integer, Nomination> constituencyNominations;
} }
@@ -1,29 +1,10 @@
package de.twomartens.wahlrecht.model.db; package de.twomartens.wahlrecht.model.db
import java.util.Map; data class VotingResult(
import lombok.AccessLevel; var electionName: String,
import lombok.AllArgsConstructor; var partyAbbreviation: String,
import lombok.Builder; var nominationName: String,
import lombok.EqualsAndHashCode; var votesOnNomination: Int = 0,
import lombok.Getter; var votesThroughHealing: Int = 0,
import lombok.NoArgsConstructor; var votesPerPosition: Map<Int, Int>
import lombok.Setter; )
import lombok.ToString;
import lombok.experimental.FieldDefaults;
@Getter
@Setter
@Builder
@EqualsAndHashCode
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class VotingResult {
String electionName;
String partyAbbreviation;
String nominationName;
int votesOnNomination;
int votesThroughHealing;
Map<Integer, Integer> votesPerPosition;
}
@@ -1,5 +1,3 @@
package de.twomartens.wahlrecht.model.internal; package de.twomartens.wahlrecht.model.internal
public record Candidate(String name, String profession) { data class Candidate(val name: String, val profession: String)
}
@@ -1,9 +1,7 @@
package de.twomartens.wahlrecht.model.internal; package de.twomartens.wahlrecht.model.internal
public record Constituency(String electionName, int number, String name, int numberOfSeats) { data class Constituency(val electionName: String, val number: Int, val name: String, val numberOfSeats: Int) {
override fun toString(): String {
@Override return name
public String toString() {
return name;
} }
} }
@@ -1,5 +1,3 @@
package de.twomartens.wahlrecht.model.internal; package de.twomartens.wahlrecht.model.internal
public record ConstituencyId(String electionName, Integer number) { data class ConstituencyId(val electionName: String, val number: Int)
}
@@ -1,6 +1,6 @@
package de.twomartens.wahlrecht.model.internal; package de.twomartens.wahlrecht.model.internal
public enum Elected { enum class Elected {
NOT_ELECTED, NOT_ELECTED,
CONSTITUENCY, CONSTITUENCY,
OVERALL_NOMINATION_ORDER, OVERALL_NOMINATION_ORDER,
@@ -1,13 +1,6 @@
package de.twomartens.wahlrecht.model.internal; package de.twomartens.wahlrecht.model.internal
data class ElectedCandidate(val candidate: Candidate, val elected: Elected) {
public record ElectedCandidate(Candidate candidate, Elected elected) { val name by candidate::name
val profession by candidate::profession
public String name() {
return candidate.name();
}
public String profession() {
return candidate.profession();
}
} }
@@ -1,9 +1,7 @@
package de.twomartens.wahlrecht.model.internal; package de.twomartens.wahlrecht.model.internal
import java.util.Collection; data class ElectionResult(
import java.util.Map; val electionName: String,
val overallResults: Collection<VotingResult>,
public record ElectionResult(String electionName, Collection<VotingResult> overallResults, val constituencyResults: Map<Int, Collection<VotingResult>>
Map<Integer, Collection<VotingResult>> constituencyResults) { )
}
@@ -23,8 +23,8 @@ data class Nomination(val id: NominationId, val supportVotesOnNomination: Boolea
return positionToCandidate.values.toList() return positionToCandidate.values.toList()
} }
fun getCandidate(position: Int): Candidate? { fun getCandidate(position: Int): Candidate {
return positionToCandidate[position] return positionToCandidate[position]!!
} }
fun supportsVotesOnNomination(): Boolean { fun supportsVotesOnNomination(): Boolean {
@@ -1,5 +1,3 @@
package de.twomartens.wahlrecht.model.internal; package de.twomartens.wahlrecht.model.internal
public record PartyId(String electionName, String abbreviation) { data class PartyId(val electionName: String, val abbreviation: String)
}
@@ -1,84 +1,83 @@
package de.twomartens.wahlrecht.monitoring.actuator; package de.twomartens.wahlrecht.monitoring.actuator
import java.io.Closeable; import mu.KotlinLogging
import java.io.IOException; import org.slf4j.MDC
import java.time.Clock; import org.springframework.boot.actuate.health.Health
import java.time.Duration; import org.springframework.boot.actuate.health.HealthIndicator
import lombok.RequiredArgsConstructor; import org.springframework.boot.actuate.health.Status
import lombok.extern.slf4j.Slf4j; import java.io.IOException
import org.slf4j.MDC; import java.time.Clock
import org.slf4j.MDC.MDCCloseable; import java.time.Duration
import org.springframework.boot.actuate.health.Health; import java.util.*
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
@Slf4j abstract class AbstractHealthIndicator(
@RequiredArgsConstructor private val clock: Clock,
public abstract class AbstractHealthIndicator implements HealthIndicator { private val preparable: Preparable
) : HealthIndicator {
public static final String HOST = "localhost"; private val logStatusDownMessage = "health indicator '${indicatorName()}' invoked with status '${Status.DOWN.code}'"
public static final String HTTP_PREFIX = "http://"; private val logStatusUpMessage = "health indicator '${indicatorName()}' invoked with status '${Status.UP.code}'"
public static final String HOST_PORT_SEPERATOR = ":";
public static final String PATH_SEPERATOR = "/";
public static final String PARAMETER_SEPERATOR = "?";
public static final String DETAIL_ENDPOINT_KEY = "endpoint";
private final String logStatusDownMessage = String.format("health indicator '%s' invoked with status '%s'", private var firstTime = true
indicatorName(), Status.DOWN.getCode());
private final String logStatusUpMessage = String.format("health indicator '%s' invoked with status '%s'",
indicatorName(), Status.UP.getCode());
private final Clock clock;
private final Preparable preparable;
private boolean firstTime = true;
/** /**
* main method that determines the health of the service * main method that determines the health of the service
*/ */
protected abstract Health determineHealth(); protected abstract fun determineHealth(): Health
@Override override fun health(): Health {
public Health health() {
try (Closeable ignored = preparable.prepare()) {
Health result = null;
Exception exception = null;
long start = clock.millis();
try { try {
result = determineHealth(); preparable.prepare().use {
} catch (RuntimeException e) { var result: Health? = null
exception = e; var exception: Exception? = null
result = Health.down().withException(e).build(); val start = clock.millis()
try {
result = determineHealth()
} catch (e: RuntimeException) {
exception = e
result = Health.down().withException(e).build()
} finally { } finally {
logInvocation(result, exception, start, clock.millis()); logInvocation(result, exception, start, clock.millis())
} }
return result; return result!!
} catch (IOException e) { }
log.error("unexpected exception occurred", e); } catch (e: IOException) {
return Health.down(e).build(); log.error("unexpected exception occurred", e)
return Health.down(e).build()
} }
} }
private void logInvocation(Health health, Exception exception, long start, long end) { private fun logInvocation(health: Health?, exception: Exception?, start: Long, end: Long) {
Duration duration = Duration.ofMillis(end - start); val duration = Duration.ofMillis(end - start)
try (MDCCloseable ignored = MDC.putCloseable("event.duration", Long.toString(duration.toNanos()))) { MDC.putCloseable("event.duration", duration.toNanos().toString()).use {
if (exception != null || health == null) { if (exception != null || health == null) {
log.error(logStatusDownMessage, exception); log.error(logStatusDownMessage, exception)
firstTime = true; firstTime = true
} else if (health.getStatus() == Status.DOWN) { } else if (health.status === Status.DOWN) {
log.warn(logStatusDownMessage); log.warn(logStatusDownMessage)
firstTime = true; firstTime = true
} else if (firstTime) { } else if (firstTime) {
log.info(logStatusUpMessage); log.info(logStatusUpMessage)
firstTime = false; firstTime = false
} else { } else {
log.trace(logStatusUpMessage); log.trace(logStatusUpMessage)
} }
} }
} }
private String indicatorName() { private fun indicatorName(): String {
return this.getClass().getSimpleName() return this.javaClass.getSimpleName()
.replace("HealthIndicator", "") .replace("HealthIndicator", "")
.toLowerCase(); .lowercase(Locale.getDefault())
} }
companion object {
const val HOST = "localhost"
const val HTTP_PREFIX = "http://"
const val HOST_PORT_SEPARATOR = ":"
const val PATH_SEPARATOR = "/"
const val PARAMETER_SEPARATOR = "?"
const val DETAIL_ENDPOINT_KEY = "endpoint"
private val log = KotlinLogging.logger {}
}
} }
@@ -1,37 +1,30 @@
package de.twomartens.wahlrecht.monitoring.actuator; package de.twomartens.wahlrecht.monitoring.actuator
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbe; import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbe
import java.time.Clock; import org.springframework.boot.actuate.health.Health
import java.util.Optional; import org.springframework.boot.actuate.health.HealthIndicator
import lombok.extern.slf4j.Slf4j; import java.time.Clock
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Health.Builder;
import org.springframework.boot.actuate.health.HealthIndicator;
@Slf4j abstract class AbstractStatusProbeHealthIndicator(
public abstract class AbstractStatusProbeHealthIndicator extends AbstractHealthIndicator timeProvider: Clock, headerInterceptor: Preparable,
implements HealthIndicator { private val statusProbe: StatusProbe
) : AbstractHealthIndicator(timeProvider, headerInterceptor), HealthIndicator {
public static final String MESSAGE_KEY = "message"; override fun determineHealth(): Health {
public static final String LAST_STATUS_CHANGE_KEY = "lastStatusChange"; val healthBuilder = Health.status(statusProbe.status)
if (statusProbe.lastStatusChange != null) {
private final StatusProbe statusProbe; healthBuilder.withDetail(LAST_STATUS_CHANGE_KEY, statusProbe.lastStatusChange)
}
public AbstractStatusProbeHealthIndicator(Clock timeProvider, Preparable headerInterceptor, if (statusProbe.throwable != null) {
StatusProbe statusProbe) { healthBuilder.withException(statusProbe.throwable)
super(timeProvider, headerInterceptor); }
this.statusProbe = statusProbe; if (statusProbe.message != null) {
healthBuilder.withDetail(MESSAGE_KEY, statusProbe.message)
}
return healthBuilder.build()
} }
@Override companion object {
protected Health determineHealth() { const val MESSAGE_KEY = "message"
Builder healthBuilder = Health.status(statusProbe.getStatus()); const val LAST_STATUS_CHANGE_KEY = "lastStatusChange"
Optional.ofNullable(statusProbe.getLastStatusChange())
.ifPresent(l -> healthBuilder.withDetail(LAST_STATUS_CHANGE_KEY, l));
Optional.ofNullable(statusProbe.getThrowable()).ifPresent(healthBuilder::withException);
Optional.ofNullable(statusProbe.getMessage())
.ifPresent(m -> healthBuilder.withDetail(MESSAGE_KEY, m));
return healthBuilder.build();
} }
} }
@@ -1,10 +1,7 @@
package de.twomartens.wahlrecht.monitoring.actuator; package de.twomartens.wahlrecht.monitoring.actuator
import java.io.Closeable; import java.io.Closeable
@FunctionalInterface
public interface Preparable {
Closeable prepare();
fun interface Preparable {
fun prepare(): Closeable
} }
@@ -1,66 +1,56 @@
package de.twomartens.wahlrecht.monitoring.actuator; package de.twomartens.wahlrecht.monitoring.actuator
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest; import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest
import lombok.extern.slf4j.Slf4j; import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.Status
import org.springframework.boot.actuate.health.Status; import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.stereotype.Component
import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate
import org.springframework.stereotype.Component; import java.security.SecureRandom
import org.springframework.web.client.RestTemplate; import java.time.Clock
import java.security.SecureRandom;
import java.time.Clock;
import java.util.Optional;
/** /**
* A Health check which checks if the rest services are working. * A Health check which checks if the rest services are working.
* <p> *
*
* If you have a complex service, you should think about an easy greeting or echo service, which * If you have a complex service, you should think about an easy greeting or echo service, which
* only tests the * only tests the
* network/service stack and not the full application. * network/service stack and not the full application.
* <p> *
*
* The health check will be called by kubernetes to check if the container/pod should be in load * The health check will be called by kubernetes to check if the container/pod should be in load
* balancing. It is possible * balancing. It is possible
* to have as much health checks as you like. * to have as much health checks as you like.
* <p> *
*
* There should be a health check which is ok not before all data is loaded. * There should be a health check which is ok not before all data is loaded.
*/ */
@Slf4j
@Component @Component
public class RestHealthIndicator extends AbstractHealthIndicator implements HealthIndicator { class RestHealthIndicator(
clock: Clock, interceptor: HeaderInterceptorRest,
private static final String URL_PATH = "/wahlrecht/v1/healthCheck"; serverProperties: ServerProperties, private val restTemplateRestHealthIndicator: RestTemplate
private static final String GET_PARAMETER = "message="; ) : AbstractHealthIndicator(clock, Preparable { interceptor.markAsHealthCheck() }), HealthIndicator {
private val randomizer = SecureRandom()
private final SecureRandom randomizer = new SecureRandom(); private val urlPrefix: String = (HTTP_PREFIX + HOST + HOST_PORT_SEPARATOR
private final RestTemplate restTemplateRestHealthIndicator; + serverProperties.port
private final String urlPrefix; + URL_PATH + PARAMETER_SEPARATOR + GET_PARAMETER)
public RestHealthIndicator(Clock clock, HeaderInterceptorRest interceptor,
ServerProperties serverProperties, RestTemplate restTemplateRestHealthIndicator) {
super(clock, interceptor::markAsHealthCheck);
this.restTemplateRestHealthIndicator = restTemplateRestHealthIndicator;
urlPrefix = HTTP_PREFIX + HOST + HOST_PORT_SEPERATOR + serverProperties.getPort()
+ URL_PATH + PARAMETER_SEPERATOR + GET_PARAMETER;
}
/** /**
* main method that determines the health of the service * main method that determines the health of the service
*/ */
@Override override fun determineHealth(): Health {
protected Health determineHealth() { val random = randomizer.nextInt(100000, 999999).toString()
String random = Integer.toString(randomizer.nextInt(100000, 999999)); val url = "$urlPrefix{random}"
String url = urlPrefix + "{random}"; val response = restTemplateRestHealthIndicator.getForEntity(url, String::class.java, random)
ResponseEntity<String> response = restTemplateRestHealthIndicator.getForEntity(url, String.class, random); val status = if (response.body == random) Status.UP else Status.DOWN
Status status = Optional.ofNullable(response.getBody())
.filter(random::equals)
.map(m -> Status.UP)
.orElse(Status.DOWN);
return Health.status(status) return Health.status(status)
.withDetail(DETAIL_ENDPOINT_KEY, url) .withDetail(DETAIL_ENDPOINT_KEY, url)
.build(); .build()
} }
companion object {
private const val URL_PATH = "/wahlrecht/v1/healthCheck"
private const val GET_PARAMETER = "message="
}
} }
@@ -1,33 +1,25 @@
package de.twomartens.wahlrecht.monitoring.statusprobe; package de.twomartens.wahlrecht.monitoring.statusprobe
import java.time.Clock; import org.springframework.boot.actuate.health.Status
import java.util.concurrent.atomic.AtomicInteger; import java.time.Clock
import org.springframework.boot.actuate.health.Status; import java.util.concurrent.atomic.AtomicInteger
public class CountBasedStatusProbe extends StatusProbe { class CountBasedStatusProbe(
private val maxFailureCount: Int, clock: Clock, criticality: StatusProbeCriticality, name: String,
statusProbeLogger: StatusProbeLogger
) : StatusProbe(clock, criticality, name, statusProbeLogger) {
private val failureCount = AtomicInteger(0)
private final AtomicInteger failureCount = new AtomicInteger(0); @Synchronized
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
private final int maxFailureCount; if (status === Status.DOWN) {
val failureCount = failureCount.incrementAndGet()
public CountBasedStatusProbe(int maxFailureCount, Clock clock, StatusProbeCriticality criticality, String name,
StatusProbeLogger statusProbeLogger) {
super(clock, criticality, name, statusProbeLogger);
this.maxFailureCount = maxFailureCount;
}
@Override
protected synchronized void setStatus(Status status, Throwable throwable, String message) {
if (status == Status.DOWN) {
int failureCount = this.failureCount.incrementAndGet();
if (failureCount > maxFailureCount) { if (failureCount > maxFailureCount) {
super.setStatus(status, throwable, message); super.setStatus(status, throwable, message)
} }
} else if (status == Status.UP) { } else if (status === Status.UP) {
this.failureCount.set(0); failureCount.set(0)
super.setStatus(status, throwable, message); super.setStatus(status, throwable, message)
} }
} }
} }
@@ -1,63 +1,51 @@
package de.twomartens.wahlrecht.monitoring.statusprobe; package de.twomartens.wahlrecht.monitoring.statusprobe
import java.time.Clock; import org.springframework.boot.actuate.health.Status
import java.time.Duration; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import org.springframework.boot.actuate.health.Status; import java.time.Clock
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import java.time.Duration
/** /**
* uses the percentage of down statuses within a given period (default: 1 min) to determine if status of probe is down. * uses the percentage of down statuses within a given period (default: 1 min) to determine if status of probe is down.
* this is meant to be used to avoid flickering status probes on services that have lots of status updates. When there * this is meant to be used to avoid flickering status probes on services that have lots of status updates. When there
* is no significant amount of requests during one scheduling period, the behavior may be arbitrary. * is no significant amount of requests during one scheduling period, the behavior may be arbitrary.
*/ */
public class PercentageBasedStatusProbe extends StatusProbe implements ScheduledStatusProbe { class PercentageBasedStatusProbe(
private val maxFailurePercent: Int, clock: Clock,
threadPoolTaskScheduler: ThreadPoolTaskScheduler, schedulePeriod: Duration, criticality: StatusProbeCriticality,
name: String, statusProbeLogger: StatusProbeLogger
) : StatusProbe(clock, criticality, name, statusProbeLogger), ScheduledStatusProbe {
private var requestCount = 0
private var downCount = 0
private var temporaryMessage: String? = null
private var temporaryThrowable: Throwable? = null
private final int maxFailurePercent; init {
scheduleTask(threadPoolTaskScheduler, schedulePeriod)
private int requestCount = 0;
private int downCount = 0;
private Throwable throwable;
private String message;
public PercentageBasedStatusProbe(int maxFailurePercent, Clock clock,
ThreadPoolTaskScheduler threadPoolTaskScheduler, StatusProbeCriticality criticality, String name,
StatusProbeLogger statusProbeLogger) {
this(maxFailurePercent, clock, threadPoolTaskScheduler, Duration.ofMinutes(1), criticality, name,
statusProbeLogger);
} }
public PercentageBasedStatusProbe(int maxFailurePercent, Clock clock, @Synchronized
ThreadPoolTaskScheduler threadPoolTaskScheduler, Duration schedulePeriod, StatusProbeCriticality criticality, override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
String name, StatusProbeLogger statusProbeLogger) { if (status === Status.DOWN) {
super(clock, criticality, name, statusProbeLogger); downCount++
this.maxFailurePercent = maxFailurePercent; this.temporaryThrowable = throwable
scheduleTask(threadPoolTaskScheduler, schedulePeriod); this.temporaryMessage = message
}
requestCount++
} }
@Override private fun reset() {
protected synchronized void setStatus(Status status, Throwable throwable, String message) { requestCount = 0
if (status == Status.DOWN) { downCount = 0
downCount++;
this.throwable = throwable;
this.message = message;
}
requestCount++;
} }
private void reset() { @Synchronized
requestCount = 0; override fun runScheduledTask() {
downCount = 0; if (requestCount > 0 && downCount * 100.0 / requestCount > maxFailurePercent) {
throwable = null; super.setStatus(Status.DOWN, temporaryThrowable, temporaryMessage)
message = null;
}
public synchronized void runScheduledTask() {
if (requestCount > 0 && (downCount * 100.0 / requestCount) > maxFailurePercent) {
super.setStatus(Status.DOWN, throwable, message);
} else { } else {
super.setStatus(Status.UP, null, null); super.setStatus(Status.UP, null, null)
} }
reset(); reset()
} }
} }
@@ -1,18 +1,22 @@
package de.twomartens.wahlrecht.monitoring.statusprobe; package de.twomartens.wahlrecht.monitoring.statusprobe
import java.time.Duration; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.support.PeriodicTrigger
import org.springframework.scheduling.support.PeriodicTrigger; import java.time.Duration
public interface ScheduledStatusProbe { interface ScheduledStatusProbe {
fun runScheduledTask()
void runScheduledTask(); fun scheduleTask(
threadPoolTaskScheduler: ThreadPoolTaskScheduler,
default void scheduleTask(ThreadPoolTaskScheduler threadPoolTaskScheduler, schedulePeriod: Duration
Duration schedulePeriod) { ) {
PeriodicTrigger periodicTrigger = new PeriodicTrigger( val periodicTrigger = PeriodicTrigger(
Duration.ofSeconds(schedulePeriod.toSeconds())); Duration.ofSeconds(schedulePeriod.toSeconds())
threadPoolTaskScheduler.schedule(this::runScheduledTask, periodicTrigger); )
threadPoolTaskScheduler.schedule(periodicTrigger) { runScheduledTask() }
} }
}
fun ThreadPoolTaskScheduler.schedule(periodicTrigger: PeriodicTrigger, task: Runnable) {
this.schedule(task, periodicTrigger)
} }
@@ -1,62 +1,59 @@
package de.twomartens.wahlrecht.monitoring.statusprobe; package de.twomartens.wahlrecht.monitoring.statusprobe
import java.time.Clock; import lombok.Getter
import java.time.ZonedDateTime; import org.springframework.boot.actuate.health.Status
import lombok.Getter; import java.time.Clock
import org.springframework.boot.actuate.health.Status; import java.time.ZonedDateTime
@Getter @Getter
public class StatusProbe { open class StatusProbe(
private val clock: Clock, criticality: StatusProbeCriticality, private val name: String,
private val statusProbeLogger: StatusProbeLogger
) {
var status: Status = Status.UP
private set
var throwable: Throwable? = null
private set
var message: String? = null
private set
var lastStatusChange: ZonedDateTime? = null
private set
private final Clock clock; init {
statusProbeLogger.registerStatusProbe(name, criticality)
private Status status = Status.UP;
private Throwable throwable = null;
private String message = null;
private ZonedDateTime lastStatusChange;
private final StatusProbeLogger statusProbeLogger;
private final String name;
public StatusProbe(Clock clock, StatusProbeCriticality criticality, String name,
StatusProbeLogger statusProbeLogger) {
this.clock = clock;
this.name = name;
this.statusProbeLogger = statusProbeLogger;
statusProbeLogger.registerStatusProbe(name, criticality);
} }
protected void setStatus(Status status, Throwable throwable, String message) { protected open fun setStatus(status: Status, throwable: Throwable?, message: String?) {
if (status != this.status) { if (status !== this.status) {
lastStatusChange = ZonedDateTime.now(clock); lastStatusChange = ZonedDateTime.now(clock)
statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable); statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable)
} }
this.status = status; this.status = status
this.throwable = throwable; this.throwable = throwable
this.message = message; this.message = message
} }
public void up() { fun up() {
setStatus(Status.UP, null, null); setStatus(Status.UP, null, null)
} }
public void up(String message) { fun up(message: String) {
setStatus(Status.UP, null, message); setStatus(Status.UP, null, message)
} }
public void down() { fun down() {
setStatus(Status.DOWN, null, null); setStatus(Status.DOWN, null, null)
} }
public void down(Throwable throwable) { fun down(throwable: Throwable) {
setStatus(Status.DOWN, throwable, null); setStatus(Status.DOWN, throwable, null)
} }
public void down(String message) { fun down(message: String) {
setStatus(Status.DOWN, null, message); setStatus(Status.DOWN, null, message)
} }
protected void down(Throwable throwable, String message) { protected fun down(throwable: Throwable, message: String) {
setStatus(Status.DOWN, throwable, message); setStatus(Status.DOWN, throwable, message)
} }
} }
@@ -1,7 +1,7 @@
package de.twomartens.wahlrecht.monitoring.statusprobe; package de.twomartens.wahlrecht.monitoring.statusprobe
enum class StatusProbeCriticality {
public enum StatusProbeCriticality { K1,
K1, K2, K3 K2,
K3
} }
@@ -1,124 +1,109 @@
package de.twomartens.wahlrecht.monitoring.statusprobe; package de.twomartens.wahlrecht.monitoring.statusprobe
import static de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality.K1; import org.apache.logging.log4j.LogManager
import static de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality.K2; import org.apache.logging.log4j.Logger
import static de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality.K3; import org.apache.logging.log4j.MarkerManager
import org.apache.logging.log4j.message.StringMapMessage
import org.springframework.boot.actuate.health.Status
import java.time.Clock
import java.time.ZonedDateTime
import java.time.Clock; class StatusProbeLogger internal constructor(private val clock: Clock, private val commLog: Logger) {
import java.time.ZonedDateTime; private val statusProbeToStatus: MutableMap<ProbeIdent, Status> = mutableMapOf()
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.message.StringMapMessage;
import org.springframework.boot.actuate.health.Status;
public class StatusProbeLogger { constructor(clock: Clock) : this(clock, LogManager.getLogger("statusprobe"))
private static final Marker MARKER = MarkerManager.getMarker("statusprobe"); fun registerStatusProbe(name: String, criticality: StatusProbeCriticality) {
statusProbeToStatus[ProbeIdent(name, criticality)] = Status.UP
private static final String LABEL_CRITICALITY = "label.status.criticality"; logStatusChange(name, "Startup", Status.UP, ZonedDateTime.now(clock), null)
private static final String LABEL_STATUS = "label.status.status";
private static final String LABEL_REASON = "label.status.reason";
private static final String LABEL_MESSAGE = "label.status.description";
private static final String LABEL_LAST_STATUS_CHANGE = "label.status.last_change";
private final Clock clock;
private final Logger commLog;
private final Map<ProbeIdent, Status> statusProbeToStatus = new HashMap<>();
public StatusProbeLogger(Clock clock) {
this(clock, LogManager.getLogger("statusprobe"));
} }
/** fun logStatusChange(
* only for testing purposes name: String, message: String?, status: Status, lastStatusChange: ZonedDateTime?,
*/ throwable: Throwable?
StatusProbeLogger(Clock clock, Logger commLog) { ) {
this.clock = clock; var probeIdent = getProbeIdent(name)
this.commLog = commLog;
}
public void registerStatusProbe(String name, StatusProbeCriticality criticality) {
statusProbeToStatus.put(new ProbeIdent(name, criticality), Status.UP);
logStatusChange(name, "Startup", Status.UP, ZonedDateTime.now(clock), null);
}
public void logStatusChange(String name, String message, Status status, ZonedDateTime lastStatusChange,
Throwable throwable) {
ProbeIdent probeIdent = getProbeIdent(name);
if (probeIdent == null) { if (probeIdent == null) {
probeIdent = new ProbeIdent(name, K1); probeIdent = ProbeIdent(name, StatusProbeCriticality.K1)
} }
statusProbeToStatus.put(probeIdent, status); statusProbeToStatus[probeIdent] = status
createLog(message, lastStatusChange, throwable); createLog(message, lastStatusChange, throwable)
} }
private ProbeIdent getProbeIdent(String name) { private fun getProbeIdent(name: String): ProbeIdent? {
return statusProbeToStatus.keySet().stream().filter(key -> key.name.equals(name)).findFirst().orElse(null); return statusProbeToStatus.keys.asSequence()
.filter { it.name == name }
.firstOrNull()
} }
private void createLog(String message, ZonedDateTime lastStatusChange, Throwable throwable) { private fun createLog(message: String?, lastStatusChange: ZonedDateTime?, throwable: Throwable?) {
Status overallStatus = getOverallStatus(); val cleanedMessage = message ?: ""
StatusProbeCriticality criticality = getOverallCriticality(); val overallStatus = overallStatus
if (message == null) { val criticality = overallCriticality
message = ""; if (Status.UP == overallStatus) {
} commLog.info(
if (Status.UP.equals(overallStatus)) { MARKER, StringMapMessage()
commLog.info(MARKER, new StringMapMessage()
.with(LABEL_CRITICALITY, criticality) .with(LABEL_CRITICALITY, criticality)
.with(LABEL_STATUS, overallStatus) .with(LABEL_STATUS, overallStatus)
.with(LABEL_MESSAGE, message) .with(LABEL_MESSAGE, cleanedMessage)
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange)); .with(LABEL_LAST_STATUS_CHANGE, lastStatusChange)
)
} else { } else {
commLog.error(MARKER, new StringMapMessage() commLog.error(
MARKER, StringMapMessage()
.with(LABEL_CRITICALITY, criticality) .with(LABEL_CRITICALITY, criticality)
.with(LABEL_STATUS, overallStatus) .with(LABEL_STATUS, overallStatus)
.with(LABEL_MESSAGE, message) .with(LABEL_MESSAGE, cleanedMessage)
.with(LABEL_REASON, getReason()) .with(LABEL_REASON, reason)
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable); .with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable
)
} }
} }
private StatusProbeCriticality getOverallCriticality() { private val overallCriticality: StatusProbeCriticality
List<StatusProbeCriticality> crits = statusProbeToStatus.keySet().stream().map(key -> key.criticality).toList(); get() {
return crits.contains(K1) ? K1 : crits.contains(K2) ? K2 : K3; val crits = statusProbeToStatus.keys.asSequence()
.map { it.criticality }
.toList()
return if (crits.contains(StatusProbeCriticality.K1)) StatusProbeCriticality.K1
else if (crits.contains(StatusProbeCriticality.K2)) StatusProbeCriticality.K2
else StatusProbeCriticality.K3
} }
private Status getOverallStatus() { private val overallStatus: Status
if (statusProbeToStatus.containsValue(Status.DOWN)) { get() = if (statusProbeToStatus.containsValue(Status.DOWN)) Status.DOWN
return Status.DOWN; else Status.UP
}
return Status.UP; private val reason: String
get() {
val probesDown = statusProbeToStatus.entries.asSequence()
.filter { it.value == Status.DOWN }
.map { it.key }
.toList()
val reasonK1 = getDownStatusProbes(probesDown, StatusProbeCriticality.K1)
val reasonK2 = getDownStatusProbes(probesDown, StatusProbeCriticality.K2)
val reasonK3 = getDownStatusProbes(probesDown, StatusProbeCriticality.K3)
return "$reasonK1$reasonK2$reasonK3".trim { it <= ' ' }
} }
private String getReason() { private fun getDownStatusProbes(probesDown: List<ProbeIdent>, criticality: StatusProbeCriticality): String {
val downProbeNames = probesDown.asSequence()
List<ProbeIdent> probesDown = statusProbeToStatus.entrySet().stream() .filter { it.criticality == criticality }
.filter(entry -> Status.DOWN.equals(entry.getValue())) .map { it.name }
.map(Map.Entry::getKey) .toList()
.toList(); return if (downProbeNames.isNotEmpty()) {
"$criticality failed: ${downProbeNames.joinToString(separator = ",")}\n"
String reasonK1 = getDownStatusProbes(probesDown, K1); } else ""
String reasonK2 = getDownStatusProbes(probesDown, K2);
String reasonK3 = getDownStatusProbes(probesDown, K3);
return "%s%s%s".formatted(reasonK1, reasonK2, reasonK3).trim();
} }
private String getDownStatusProbes(List<ProbeIdent> probesDown, StatusProbeCriticality criticality) { data class ProbeIdent(val name: String, val criticality: StatusProbeCriticality)
List<String> downProbeNames = probesDown.stream().filter(probe -> probe.criticality.equals(criticality))
.map(probe -> probe.name).toList();
if (downProbeNames.size() > 0) {
return criticality + " failed: " + String.join(",", downProbeNames) + "\n";
}
return "";
}
public record ProbeIdent(String name, StatusProbeCriticality criticality) {
companion object {
private val MARKER = MarkerManager.getMarker("statusprobe")
private const val LABEL_CRITICALITY = "label.status.criticality"
private const val LABEL_STATUS = "label.status.status"
private const val LABEL_REASON = "label.status.reason"
private const val LABEL_MESSAGE = "label.status.description"
private const val LABEL_LAST_STATUS_CHANGE = "label.status.last_change"
} }
} }
@@ -1,64 +1,48 @@
package de.twomartens.wahlrecht.monitoring.statusprobe; package de.twomartens.wahlrecht.monitoring.statusprobe
import java.time.Clock; import org.springframework.boot.actuate.health.Status
import java.time.Duration; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.time.ZonedDateTime; import java.time.Clock
import org.springframework.boot.actuate.health.Status; import java.time.Duration
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import java.time.ZonedDateTime
public class TimeBasedStatusProbe extends StatusProbe implements ScheduledStatusProbe { class TimeBasedStatusProbe(
private val maxFailureDuration: Duration, private val clock: Clock,
threadPoolTaskScheduler: ThreadPoolTaskScheduler, schedulePeriod: Duration, criticality: StatusProbeCriticality,
name: String, statusProbeLogger: StatusProbeLogger
) : StatusProbe(clock, criticality, name, statusProbeLogger), ScheduledStatusProbe {
private var lastSuccess: ZonedDateTime? = null
private var temporaryThrowable: Throwable? = null
private var temporaryMessage: String? = null
private final Clock clock; init {
scheduleTask(threadPoolTaskScheduler, schedulePeriod)
private final Duration maxFailureDuration;
private ZonedDateTime lastSuccess;
private Throwable throwable = null;
private String message = null;
public TimeBasedStatusProbe(Duration maxFailureDuration, Clock clock,
ThreadPoolTaskScheduler threadPoolTaskScheduler, StatusProbeCriticality criticality, String name,
StatusProbeLogger statusProbeLogger) {
this(maxFailureDuration, clock, threadPoolTaskScheduler, Duration.ofMinutes(1), criticality, name,
statusProbeLogger);
} }
public TimeBasedStatusProbe(Duration maxFailureDuration, Clock clock, @Synchronized
ThreadPoolTaskScheduler threadPoolTaskScheduler, Duration schedulePeriod, StatusProbeCriticality criticality, override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
String name, StatusProbeLogger statusProbeLogger) { if (status === Status.DOWN) {
super(clock, criticality, name, statusProbeLogger); this.temporaryThrowable = throwable
this.clock = clock; this.temporaryMessage = message
this.maxFailureDuration = maxFailureDuration; } else if (status === Status.UP) {
this.lastSuccess = null; lastSuccess = ZonedDateTime.now(clock)
scheduleTask(threadPoolTaskScheduler, schedulePeriod); super.setStatus(status, throwable, message)
}
@Override
protected synchronized void setStatus(Status status, Throwable throwable, String message) {
if (status == Status.DOWN) {
this.throwable = throwable;
this.message = message;
} else if (status == Status.UP) {
lastSuccess = ZonedDateTime.now(clock);
super.setStatus(status, throwable, message);
} }
} }
private boolean isOverdue() { private val isOverdue: Boolean
get() {
if (lastSuccess == null) { if (lastSuccess == null) {
return false; return false
} }
Duration timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock)); val timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock))
return maxFailureDuration.minus(timeSinceLastSuccess).isNegative(); return maxFailureDuration.minus(timeSinceLastSuccess).isNegative
} }
public synchronized void runScheduledTask() { @Synchronized
if (isOverdue()) { override fun runScheduledTask() {
super.setStatus(Status.DOWN, throwable, message); if (isOverdue) {
super.setStatus(Status.DOWN, temporaryThrowable, temporaryMessage)
} }
} }
} }
@@ -1,10 +1,6 @@
package de.twomartens.wahlrecht.repository; package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.Candidate; import de.twomartens.wahlrecht.model.db.Candidate
import java.util.Optional; import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.data.mongodb.repository.MongoRepository;
public interface CandidateRepository extends MongoRepository<Candidate, String> { interface CandidateRepository : MongoRepository<Candidate, String>
Optional<Candidate> findByName(String name);
}
@@ -1,13 +1,6 @@
package de.twomartens.wahlrecht.repository; package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.Constituency; import de.twomartens.wahlrecht.model.db.Constituency
import java.util.Collection; import org.springframework.data.mongodb.repository.MongoRepository
import java.util.Optional;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface ConstituencyRepository extends MongoRepository<Constituency, String> { interface ConstituencyRepository : MongoRepository<Constituency, String>
Optional<Constituency> findByElectionNameAndNumber(String electionName, int number);
Collection<Constituency> findByElectionName(String electionName);
}
@@ -1,10 +1,6 @@
package de.twomartens.wahlrecht.repository; package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.Election; import de.twomartens.wahlrecht.model.db.Election
import java.util.Optional; import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.data.mongodb.repository.MongoRepository;
public interface ElectionRepository extends MongoRepository<Election, String> { interface ElectionRepository : MongoRepository<Election, String>
Optional<Election> findByName(String name);
}
@@ -1,7 +1,6 @@
package de.twomartens.wahlrecht.repository; package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.ElectionResult; import de.twomartens.wahlrecht.model.db.ElectionResult
import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.MongoRepository
public interface ElectionResultRepository extends MongoRepository<ElectionResult, String> { interface ElectionResultRepository : MongoRepository<ElectionResult, String>
}
@@ -1,12 +1,6 @@
package de.twomartens.wahlrecht.repository; package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.Nomination; import de.twomartens.wahlrecht.model.db.Nomination
import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.MongoRepository
import java.util.Optional; interface NominationRepository : MongoRepository<Nomination, String>
public interface NominationRepository extends MongoRepository<Nomination, String> {
Optional<Nomination> findByNameAndElectionNameAndPartyAbbreviation(String name,
String electionName, String partyAbbreviation);
}
@@ -1,14 +1,8 @@
package de.twomartens.wahlrecht.repository; package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.PartyInElection; import de.twomartens.wahlrecht.model.db.PartyInElection
import java.util.Collection; import org.springframework.data.mongodb.repository.MongoRepository
import java.util.Optional;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface PartyRepository extends MongoRepository<PartyInElection, String> { interface PartyRepository : MongoRepository<PartyInElection, String> {
fun findByElectionName(name: String): Collection<PartyInElection>
Optional<PartyInElection> findByAbbreviationAndElectionName(String abbreviation,
String electionName);
Collection<PartyInElection> findByElectionName(String name);
} }
@@ -12,18 +12,16 @@ import org.keycloak.adapters.authorization.spi.ConfigurationResolver
import org.keycloak.adapters.authorization.spi.HttpRequest import org.keycloak.adapters.authorization.spi.HttpRequest
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
import java.io.IOException import java.io.IOException
import java.util.*
class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver?) : Filter { class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver) : Filter {
private val policyEnforcer: MutableMap<PolicyEnforcerConfig, SpringPolicyEnforcer> = Collections.synchronizedMap(HashMap()) private val policyEnforcer: MutableMap<PolicyEnforcerConfig, SpringPolicyEnforcer> = mutableMapOf()
@Throws(IOException::class, ServletException::class) @Throws(IOException::class, ServletException::class)
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) { override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) {
val request = servletRequest as HttpServletRequest val request = servletRequest as HttpServletRequest
val response = servletResponse as HttpServletResponse? val response = servletResponse as HttpServletResponse?
val httpRequest = ServletHttpRequest(request val httpRequest = ServletHttpRequest(request) { extractBearerToken(request) }
) { extractBearerToken(request) }
val policyEnforcer = getOrCreatePolicyEnforcer(httpRequest) val policyEnforcer = getOrCreatePolicyEnforcer(httpRequest)
val authzContext = policyEnforcer.enforce(httpRequest, ServletHttpResponse(response)) val authzContext = policyEnforcer.enforce(httpRequest, ServletHttpResponse(response))
request.setAttribute(AuthorizationContext::class.java.name, authzContext) request.setAttribute(AuthorizationContext::class.java.name, authzContext)
@@ -55,9 +53,7 @@ class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolv
} }
private fun getOrCreatePolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer { private fun getOrCreatePolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer {
return policyEnforcer.computeIfAbsent(configResolver!!.resolve(request)) { enforcerConfig: PolicyEnforcerConfig -> return policyEnforcer.computeIfAbsent(configResolver.resolve(request)) { createPolicyEnforcer(it) }
createPolicyEnforcer(enforcerConfig)
}
} }
private fun createPolicyEnforcer(enforcerConfig: PolicyEnforcerConfig): SpringPolicyEnforcer { private fun createPolicyEnforcer(enforcerConfig: PolicyEnforcerConfig): SpringPolicyEnforcer {
@@ -1,43 +1,35 @@
package de.twomartens.wahlrecht.service; package de.twomartens.wahlrecht.service
import de.twomartens.wahlrecht.model.db.Candidate; import de.twomartens.wahlrecht.model.db.Candidate
import de.twomartens.wahlrecht.repository.CandidateRepository; import de.twomartens.wahlrecht.repository.CandidateRepository
import java.util.Map; import org.springframework.stereotype.Service
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service @Service
public class CandidateService { class CandidateService(private val repository: CandidateRepository) {
private val candidates: MutableMap<String, Candidate> by lazy {
private final CandidateRepository repository; fetchCandidates()
private Map<String, Candidate> candidates;
public Candidate storeCandidate(@NonNull Candidate candidate) {
if (candidates == null) {
fetchCandidates();
}
Candidate existing = candidates.get(candidate.getName());
boolean needsUpdate = !candidate.equals(existing);
if (!needsUpdate) {
return existing;
} }
fun storeCandidate(candidate: Candidate): Candidate {
var result = candidate
val existing = candidates[candidate.name]
val needsUpdate = candidate != existing
if (!needsUpdate && existing != null) {
return existing
}
if (existing != null) { if (existing != null) {
existing.setProfession(candidate.getProfession()); existing.profession = candidate.profession
candidate = existing; result = existing
}
result = repository.save(result)
candidates[candidate.name] = candidate
return result
} }
candidate = repository.save(candidate); private fun fetchCandidates(): MutableMap<String, Candidate> {
candidates.put(candidate.getName(), candidate); return repository.findAll().asSequence()
return candidate; .map { Pair(it.name, it) }
} .toMap()
.toMutableMap()
private void fetchCandidates() {
candidates = repository.findAll().stream()
.collect(Collectors.toMap(Candidate::getName, Function.identity()));
} }
} }
@@ -1,52 +1,40 @@
package de.twomartens.wahlrecht.service; package de.twomartens.wahlrecht.service
import de.twomartens.wahlrecht.model.db.Constituency; import de.twomartens.wahlrecht.model.db.Constituency
import de.twomartens.wahlrecht.model.internal.ConstituencyId; import de.twomartens.wahlrecht.model.internal.ConstituencyId
import de.twomartens.wahlrecht.repository.ConstituencyRepository; import de.twomartens.wahlrecht.repository.ConstituencyRepository
import java.util.Map; import org.springframework.stereotype.Service
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
@Service @Service
@RequiredArgsConstructor class ConstituencyService(private val repository: ConstituencyRepository) {
public class ConstituencyService { private val constituencies: MutableMap<ConstituencyId, Constituency> by lazy {
fetchConstituencies()
private final ConstituencyRepository repository;
private Map<ConstituencyId, Constituency> constituencies;
public Constituency storeConstituency(@NonNull Constituency constituency) {
if (constituencies == null) {
fetchConstituencies();
} }
ConstituencyId constituencyId = new ConstituencyId(constituency.getElectionName(), fun storeConstituency(constituency: Constituency): Constituency {
constituency.getNumber()); var result = constituency
Constituency existing = constituencies.get(constituencyId); val constituencyId = ConstituencyId(
constituency.electionName,
if (constituency.equals(existing)) { constituency.number
return existing; )
val existing = constituencies[constituencyId]
if (constituency == existing) {
return existing
} }
if (existing != null) { if (existing != null) {
existing.setNumberOfSeats(constituency.getNumberOfSeats()); existing.numberOfSeats = constituency.numberOfSeats
existing.setName(constituency.getName()); existing.name = constituency.name
constituency = existing; result = existing
} }
Constituency stored = repository.save(constituency); val stored = repository.save(result)
constituencies.put(constituencyId, stored); constituencies[constituencyId] = stored
return stored
return stored;
} }
private void fetchConstituencies() { private fun fetchConstituencies(): MutableMap<ConstituencyId, Constituency> {
constituencies = repository.findAll().stream() return repository.findAll().asSequence()
.collect(Collectors.toMap( .map { Pair(ConstituencyId(electionName = it.electionName, number = it.number), it) }
constituency -> new ConstituencyId(constituency.getElectionName(), .toMap()
constituency.getNumber()), .toMutableMap()
Function.identity()));
} }
} }
@@ -1,66 +1,49 @@
package de.twomartens.wahlrecht.service; package de.twomartens.wahlrecht.service
import de.twomartens.wahlrecht.model.db.ElectionResult; import de.twomartens.wahlrecht.model.db.ElectionResult
import de.twomartens.wahlrecht.repository.ElectionResultRepository; import de.twomartens.wahlrecht.repository.ElectionResultRepository
import java.util.Map; import org.springframework.http.HttpStatus
import java.util.function.Function; import org.springframework.stereotype.Service
import java.util.stream.Collectors; import org.springframework.web.server.ResponseStatusException
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@RequiredArgsConstructor
@Service @Service
public class ElectionResultService { class ElectionResultService(private val repository: ElectionResultRepository) {
private val electionResults: MutableMap<String, ElectionResult> by lazy {
private final ElectionResultRepository repository; fetchResults()
private Map<String, ElectionResult> electionResults;
public ElectionResult getElectionResult(String electionName) {
if (electionResults == null) {
fetchResults();
} }
ElectionResult electionResult = electionResults.get(electionName); fun getElectionResult(electionName: String): ElectionResult {
if (electionResult == null) { return electionResults[electionName]
throw new ResponseStatusException(HttpStatus.NOT_FOUND, ?: throw ResponseStatusException(
"no election result found for %s".formatted(electionName)); HttpStatus.NOT_FOUND,
} "no election result found for $electionName"
return electionResult; )
} }
public boolean storeResult(ElectionResult result) { fun storeResult(electionResult: ElectionResult): Boolean {
if (electionResults == null) { var result = electionResult
fetchResults();
}
String id = result.getElectionName();
ElectionResult existing = electionResults.get(id);
boolean needsUpdate = !result.equals(existing);
val id = result.electionName
val existing = electionResults[id]
val needsUpdate = result != existing
if (!needsUpdate) { if (!needsUpdate) {
return false; return false
} }
var createdNew = true
boolean createdNew = true;
if (existing != null) { if (existing != null) {
existing.setOverallResults(result.getOverallResults()); existing.overallResults = result.overallResults
existing.setConstituencyResults(result.getConstituencyResults()); existing.constituencyResults = result.constituencyResults
result = existing; result = existing
createdNew = false; createdNew = false
}
val stored = repository.save(result)
electionResults[id] = stored
return createdNew
} }
ElectionResult stored = repository.save(result); private fun fetchResults(): MutableMap<String, ElectionResult> {
electionResults.put(id, stored); return repository.findAll().asSequence()
.map { Pair(it.electionName, it) }
return createdNew; .toMap()
} .toMutableMap()
private void fetchResults() {
electionResults = repository.findAll().stream()
.collect(Collectors.toMap(
ElectionResult::getElectionName,
Function.identity()));
} }
} }
@@ -1,78 +1,60 @@
package de.twomartens.wahlrecht.service; package de.twomartens.wahlrecht.service
import de.twomartens.wahlrecht.mapper.v1.ElectionMapper; import de.twomartens.wahlrecht.mapper.v1.ElectionMapper
import de.twomartens.wahlrecht.model.db.Constituency; import de.twomartens.wahlrecht.model.db.Constituency
import de.twomartens.wahlrecht.model.db.Election; import de.twomartens.wahlrecht.model.db.Election
import de.twomartens.wahlrecht.repository.ElectionRepository; import de.twomartens.wahlrecht.repository.ElectionRepository
import java.util.ArrayList; import org.mapstruct.factory.Mappers
import java.util.Collection; import org.springframework.stereotype.Service
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.mapstruct.factory.Mappers;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service @Service
public class ElectionService { class ElectionService(
private val electionRepository: ElectionRepository,
private final ElectionRepository electionRepository; private val constituencyService: ConstituencyService
private final ConstituencyService constituencyService; ) {
private final ElectionMapper electionMapper = Mappers.getMapper(ElectionMapper.class); private val electionMapper = Mappers.getMapper(ElectionMapper::class.java)
private val _elections: MutableMap<String, Election> by lazy {
private Map<String, Election> elections; fetchElections()
public Collection<Election> getElections() {
return electionRepository.findAll();
} }
public Election getElection(String electionName) { val elections: Map<String, Election> = _elections
if (elections == null) {
fetchElections(); private fun getElection(electionName: String): Election? {
} return elections[electionName]
return elections.get(electionName);
} }
public de.twomartens.wahlrecht.model.internal.Election getElectionInternal(String electionName) { fun getElectionInternal(electionName: String): de.twomartens.wahlrecht.model.internal.Election {
return electionMapper.mapToInternal(getElection(electionName)); return electionMapper.mapToInternal(getElection(electionName)!!)
} }
public boolean storeElection(@NonNull Election election) { fun storeElection(election: Election): Boolean {
if (elections == null) { var result = election
fetchElections(); var createdNew = true
val electionName = election.name
val existing = elections[electionName]
if (election == existing) {
return false
} }
boolean createdNew = true; val constituencies: MutableCollection<Constituency> = mutableListOf()
String electionName = election.getName(); result.constituencies
Election existing = elections.get(electionName); .forEach { constituencies.add(constituencyService.storeConstituency(it)) }
if (election.equals(existing)) {
return false;
}
Collection<Constituency> constituencies = new ArrayList<>();
election.getConstituencies()
.forEach(constituency -> constituencies.add(
constituencyService.storeConstituency(constituency)));
if (existing != null) { if (existing != null) {
existing.setDay(election.getDay()); existing.day = election.day
existing.setTotalNumberOfSeats(election.getTotalNumberOfSeats()); existing.totalNumberOfSeats = election.totalNumberOfSeats
existing.setVotingThreshold(election.getVotingThreshold()); existing.votingThreshold = election.votingThreshold
election = existing; result = existing
createdNew = false; createdNew = false
}
result.constituencies = constituencies
val stored = electionRepository.save(result)
_elections[electionName] = stored
return createdNew
} }
election.setConstituencies(constituencies); private fun fetchElections(): MutableMap<String, Election> {
Election stored = electionRepository.save(election); return electionRepository.findAll().asSequence()
elections.put(electionName, stored); .map { Pair(it.name, it) }
.toMap()
return createdNew; .toMutableMap()
}
private void fetchElections() {
elections = electionRepository.findAll().stream()
.collect(Collectors.toMap(Election::getName, Function.identity()));
} }
} }
@@ -1,77 +1,66 @@
package de.twomartens.wahlrecht.service; package de.twomartens.wahlrecht.service
import de.twomartens.wahlrecht.model.db.Nomination; import de.twomartens.wahlrecht.model.db.Nomination
import de.twomartens.wahlrecht.model.db.PartyInElection; import de.twomartens.wahlrecht.model.db.PartyInElection
import de.twomartens.wahlrecht.model.internal.PartyId; import de.twomartens.wahlrecht.model.internal.PartyId
import de.twomartens.wahlrecht.repository.PartyRepository; import de.twomartens.wahlrecht.repository.PartyRepository
import java.util.Collection; import org.springframework.http.HttpStatus
import java.util.HashMap; import org.springframework.stereotype.Service
import java.util.List; import org.springframework.web.server.ResponseStatusException
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@RequiredArgsConstructor
@Service @Service
public class PartyService { class PartyService(
private val partyRepository: PartyRepository,
private val nominationService: NominationService
) {
private final PartyRepository partyRepository; private val parties: MutableMap<PartyId, PartyInElection> by lazy {
private final NominationService nominationService; fetchParties()
private Map<PartyId, PartyInElection> parties = null; }
public Collection<PartyInElection> getPartiesByElectionName(String electionName) { fun getPartiesByElectionName(electionName: String): Collection<PartyInElection> {
Collection<PartyInElection> parties = partyRepository.findByElectionName(electionName); val parties = partyRepository.findByElectionName(electionName)
if (parties.isEmpty()) { if (parties.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "no party found for " + electionName); throw ResponseStatusException(HttpStatus.NOT_FOUND, "no party found for $electionName")
}
return parties
} }
return parties; fun storeParty(partyInElection: PartyInElection): Boolean {
} var result = partyInElection
var createdNew = true
public boolean storeParty(PartyInElection partyInElection) { val partyId = PartyId(
if (parties == null) { partyInElection.electionName,
fetchParties(); partyInElection.abbreviation
} )
val existing = parties[partyId]
boolean createdNew = true; val needsUpdate = partyInElection != existing
PartyId partyId = new PartyId(partyInElection.getElectionName(),
partyInElection.getAbbreviation());
PartyInElection existing = parties.get(partyId);
boolean needsUpdate = !partyInElection.equals(existing);
if (!needsUpdate) { if (!needsUpdate) {
return false; return false
} }
val constituencyNominations: MutableMap<Int, Nomination?> = mutableMapOf()
Map<Integer, Nomination> constituencyNominations = new HashMap<>(); result.constituencyNominations
partyInElection.getConstituencyNominations() .forEach { constituencyNominations[it.key] = nominationService.storeNomination(it.value) }
.forEach((key, value) -> constituencyNominations.put(key,
nominationService.storeNomination(value)));
if (existing != null) { if (existing != null) {
existing.setName(partyInElection.getName()); existing.name = partyInElection.name
if (constituencyNominations.isEmpty()) { if (constituencyNominations.isEmpty()) {
constituencyNominations.putAll(existing.getConstituencyNominations()); constituencyNominations.putAll(existing.constituencyNominations)
} }
partyInElection = existing; result = existing
createdNew = false; createdNew = false
} }
partyInElection.setConstituencyNominations(constituencyNominations); partyInElection.constituencyNominations = constituencyNominations
partyInElection.setOverallNomination( partyInElection.overallNomination = nominationService.storeNomination(partyInElection.overallNomination)!!
nominationService.storeNomination(partyInElection.getOverallNomination())); result = partyRepository.save(result)
partyInElection = partyRepository.save(partyInElection); parties[partyId] = result
parties.put(partyId, partyInElection); return createdNew
return createdNew;
} }
private void fetchParties() { private fun fetchParties(): MutableMap<PartyId, PartyInElection> {
List<PartyInElection> allParties = partyRepository.findAll(); val allParties = partyRepository.findAll()
parties = allParties.stream() return allParties.asSequence()
.collect(Collectors.toMap( .map { Pair(PartyId(electionName = it.electionName, abbreviation = it.abbreviation), it) }
p -> new PartyId(p.getElectionName(), p.getAbbreviation()), .toMap()
Function.identity())); .toMutableMap()
} }
} }
@@ -1,37 +1,41 @@
package de.twomartens.wahlrecht.service; package de.twomartens.wahlrecht.service
import java.util.Set; import mu.KotlinLogging
import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.message.StringMapMessage
import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier
import lombok.extern.log4j.Log4j2; import org.springframework.cloud.context.refresh.ContextRefresher
import org.apache.logging.log4j.message.StringMapMessage; import org.springframework.scheduling.annotation.Scheduled
import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.stereotype.Service
import org.springframework.scheduling.annotation.Scheduled; import java.util.concurrent.TimeUnit
import org.springframework.stereotype.Service;
/** /**
* automatically reloads application*.yaml. reload can also be triggered manually by doing a post * automatically reloads application*.yaml. reload can also be triggered manually by doing a post
* request on http://localhost:12001/actuator/refresh * request on http://localhost:12001/actuator/refresh
*/ */
@Log4j2
@Service @Service
@RequiredArgsConstructor class PropertyReloadService(@Qualifier("configDataContextRefresher") private val contextRefresher: ContextRefresher) {
public class PropertyReloadService { @Scheduled(
fixedDelay = REFRESH_SECONDS.toLong(),
public static final int REFRESH_SECONDS = 60; initialDelay = REFRESH_SECONDS.toLong(),
public static final String PARAM_MESSAGE = "message"; timeUnit = TimeUnit.SECONDS
public static final String PARAM_PROPERTIES = "labels.properties"; )
fun refresh() {
private final ContextRefresher contextRefresher; val properties = contextRefresher.refresh()
if (properties.isNotEmpty()) {
@Scheduled(fixedDelay = REFRESH_SECONDS, initialDelay = REFRESH_SECONDS, timeUnit = TimeUnit.SECONDS) log.info(
public void refresh() { StringMapMessage()
Set<String> properties = contextRefresher.refresh();
if (!properties.isEmpty()) {
log.info(new StringMapMessage()
.with(PARAM_MESSAGE, "properties changed") .with(PARAM_MESSAGE, "properties changed")
.with(PARAM_PROPERTIES, String.join("\n", properties))); .with(PARAM_PROPERTIES, properties.joinToString("\n"))
.asString()
)
} }
} }
companion object {
const val REFRESH_SECONDS = 60
const val PARAM_MESSAGE = "message"
const val PARAM_PROPERTIES = "labels.properties"
private val log = KotlinLogging.logger {}
}
} }
@@ -41,6 +41,11 @@ spring:
de.twomartens.wahlrecht: de.twomartens.wahlrecht:
defaultTimeZone: Europe/Berlin defaultTimeZone: Europe/Berlin
greeting: "Hello %s" greeting: "Hello %s"
statusprobe:
scheduleDuration: 60
maxKafkaFailureDuration: 5
maxBlobFailureCount: 5
maxFailurePercent: 50
springdoc: springdoc:
swagger-ui: swagger-ui:
@@ -11,7 +11,6 @@ import org.mockito.Mockito
import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoExtension
import java.time.LocalDate import java.time.LocalDate
import java.time.Month import java.time.Month
import java.util.*
import kotlin.collections.set import kotlin.collections.set
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
@@ -261,12 +260,9 @@ class CalculationServiceTest {
} }
private fun buildCandidates(vararg names: String): Collection<Candidate> { private fun buildCandidates(vararg names: String): Collection<Candidate> {
return Arrays.stream(names).map { name: String? -> return names
Candidate( .map { Candidate(it, "") }
name, .toList()
""
)
}.toList()
} }
private fun buildVotesPerPosition(vararg votes: Int): Map<Int, Int> { private fun buildVotesPerPosition(vararg votes: Int): Map<Int, Int> {
@@ -339,13 +335,13 @@ class CalculationServiceTest {
@Test @Test
fun shouldCalculateOverallCandidatesCorrectly() { fun shouldCalculateOverallCandidatesCorrectly() {
val seatsPerVotingResult = java.util.Map.of( val seatsPerVotingResult = mapOf(
SPD_BEZIRK_RESULT, 2, Pair(SPD_BEZIRK_RESULT, 2),
CDU_BEZIRK_RESULT, 2, Pair(CDU_BEZIRK_RESULT, 2),
LINKE_BEZIRK_RESULT, 3, Pair(LINKE_BEZIRK_RESULT, 3),
GRUENE_BEZIRK_RESULT, 8, Pair(GRUENE_BEZIRK_RESULT, 8),
FDP_BEZIRK_RESULT, 3, Pair(FDP_BEZIRK_RESULT, 3),
AFD_BEZIRK_RESULT, 3 Pair(AFD_BEZIRK_RESULT, 3)
) )
val electedCandidates = setUpConstituencyResults() val electedCandidates = setUpConstituencyResults()
Mockito.`when`(nominationService.getNominationInternal(SPD_BEZIRK.id)).thenReturn(SPD_BEZIRK) Mockito.`when`(nominationService.getNominationInternal(SPD_BEZIRK.id)).thenReturn(SPD_BEZIRK)
@@ -363,13 +359,13 @@ class CalculationServiceTest {
Assertions.assertThat(id).isEqualTo(SPD_BEZIRK.id) Assertions.assertThat(id).isEqualTo(SPD_BEZIRK.id)
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(GOTTLIEB_GABOR) (candidate.name == GOTTLIEB_GABOR
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(RIEGEL_ANN_KATHRIN) (candidate.name == RIEGEL_ANN_KATHRIN
&& candidate.elected === Elected.OVERALL_VOTE_ORDER) && candidate.elected == Elected.OVERALL_VOTE_ORDER)
} }
} }
.anySatisfy { id, candidates -> .anySatisfy { id, candidates ->
@@ -377,14 +373,13 @@ class CalculationServiceTest {
.isEqualTo(CDU_BEZIRK.id) .isEqualTo(CDU_BEZIRK.id)
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(HOEFLICH_JUTTA) (candidate.name == HOEFLICH_JUTTA
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name() (candidate.name == DR_LANGHEIN_A_W_HEINRICH
.equals(DR_LANGHEIN_A_W_HEINRICH) && candidate.elected == Elected.OVERALL_VOTE_ORDER)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
} }
} }
.anySatisfy { id, candidates -> .anySatisfy { id, candidates ->
@@ -392,18 +387,18 @@ class CalculationServiceTest {
.isEqualTo(LINKE_BEZIRK.id) .isEqualTo(LINKE_BEZIRK.id)
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(PAGELS_MANUELA) (candidate.name == PAGELS_MANUELA
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(WIEGMANN_ROLAND) (candidate.name == WIEGMANN_ROLAND
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(KLEINERT_MIKEY) (candidate.name == KLEINERT_MIKEY
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER) && candidate.elected == Elected.OVERALL_VOTE_ORDER)
} }
} }
.anySatisfy { id, candidates -> .anySatisfy { id, candidates ->
@@ -411,19 +406,18 @@ class CalculationServiceTest {
.isEqualTo(FDP_BEZIRK.id) .isEqualTo(FDP_BEZIRK.id)
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(KRUEGER_KLAUS) (candidate.name == KRUEGER_KLAUS
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(SCHWANKE_BENJAMIN) (candidate.name == SCHWANKE_BENJAMIN
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name() (candidate.name == MUELLER_SOENKSEN_BURKHARDT
.equals(MUELLER_SOENKSEN_BURKHARDT) && candidate.elected == Elected.OVERALL_VOTE_ORDER)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
} }
} }
.anySatisfy { id, candidates -> .anySatisfy { id, candidates ->
@@ -431,18 +425,18 @@ class CalculationServiceTest {
.isEqualTo(AFD_BEZIRK.id) .isEqualTo(AFD_BEZIRK.id)
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(SCHOEMER_DIRK) (candidate.name == SCHOEMER_DIRK
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(ZIMMERMANN_ELKE) (candidate.name == ZIMMERMANN_ELKE
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(PILLATZKE_JOERG) (candidate.name == PILLATZKE_JOERG
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER) && candidate.elected == Elected.OVERALL_VOTE_ORDER)
} }
} }
.anySatisfy { id, candidates -> .anySatisfy { id, candidates ->
@@ -450,50 +444,50 @@ class CalculationServiceTest {
.isEqualTo(GRUENE_BEZIRK.id) .isEqualTo(GRUENE_BEZIRK.id)
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(KUHLMANN_DIETMAR) (candidate.name == KUHLMANN_DIETMAR
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(KLEIN_ROBERT) (candidate.name == KLEIN_ROBERT
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(ERK_ARAMAK) (candidate.name == ERK_ARAMAK
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(MARTENS_JIM) (candidate.name == MARTENS_JIM
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name() (candidate.name == DR_FISCHER_JOST_LEONHARDT
.equals(DR_FISCHER_JOST_LEONHARDT) && candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(KUELL_GABRIELA) (candidate.name == KUELL_GABRIELA
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER) && candidate.elected == Elected.OVERALL_VOTE_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(BOHNY_CARL_MARIA) (candidate.name == BOHNY_CARL_MARIA
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER) && candidate.elected == Elected.OVERALL_VOTE_ORDER)
} }
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch { candidate -> .anyMatch { candidate ->
(candidate.name().equals(DORSCH_SEBASTIAN) (candidate.name == DORSCH_SEBASTIAN
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER) && candidate.elected == Elected.OVERALL_VOTE_ORDER)
} }
} }
} }
private fun setUpConstituencyResults(): Map<String, Collection<ElectedCandidate>> { private fun setUpConstituencyResults(): Map<String, Collection<ElectedCandidate>> {
return java.util.Map.of( return mapOf(
Pair(
GRUENE_BEZIRK_RESULT.nominationId.partyAbbreviation, listOf( GRUENE_BEZIRK_RESULT.nominationId.partyAbbreviation, listOf(
ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY),
@@ -508,5 +502,6 @@ class CalculationServiceTest {
ElectedCandidate(GRUENE_BEZIRK.getCandidate(22), Elected.CONSTITUENCY) ElectedCandidate(GRUENE_BEZIRK.getCandidate(22), Elected.CONSTITUENCY)
) )
) )
)
} }
} }