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())
} }
@@ -69,4 +68,16 @@ tasks.bootJar {
tasks.jar { 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)
} }
+2 -1
View File
@@ -6,4 +6,5 @@ org.gradle.parallel=true
org.gradle.daemon=true 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 {
@@ -10,8 +9,4 @@ dependencies {
implementation(libs.bundles.spring.boot.security) implementation(libs.bundles.spring.boot.security)
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 {
.title(apiTitle) return OpenAPI()
.version(apiVersion) .info(
.description(apiDesciption)); Info()
} .title(apiTitle)
.version(apiVersion)
.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 { @Bean
open fun statusProbeLogger(): StatusProbeLogger {
return StatusProbeLogger(clock)
}
private final Clock clock; @Bean
open fun testStatusProbe(statusProbeLogger: StatusProbeLogger): StatusProbe {
@Bean return CountBasedStatusProbe(
public StatusProbeLogger statusProbeLogger() { 1,
return new StatusProbeLogger(clock); clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger
} )
}
@Bean
public StatusProbe testStatusProbe(StatusProbeLogger statusProbeLogger) {
return new CountBasedStatusProbe(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
public void addCorsMappings(@NonNull CorsRegistry registry) {
CorsRegistration registration = registry.addMapping("/**");
registration.allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(),
HttpMethod.PUT.name(), HttpMethod.OPTIONS.name());
registration.allowCredentials(true);
registration.allowedOrigins("http://localhost:4200");
}
override fun addCorsMappings(registry: CorsRegistry) {
val registration = registry.addMapping("/**")
registration.allowedMethods(
HttpMethod.GET.name(), HttpMethod.POST.name(),
HttpMethod.PUT.name(), HttpMethod.OPTIONS.name()
)
registration.allowCredentials(true)
registration.allowedOrigins("http://localhost:4200")
}
} }
@@ -1,112 +1,102 @@
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 lateinit var jwkSetUri: String
private static final Collection<String> PERMITTED_PATHS = List.of( @Value("#{environment.CLIENT_SECRET}")
"/wahlrecht/v1/healthCheck", private lateinit var clientSecret: String
"/actuator/**",
"/wahlrecht/v1/doc/**",
"/wahlrecht/v1/api-docs/**",
"/error");
private static final List<PathConfig> PATHS = buildPathConfigs(); @Bean
@Throws(Exception::class)
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") http
private String jwkSetUri; .csrf().disable()
.authorizeHttpRequests()
@Value("#{environment.CLIENT_SECRET}") .requestMatchers(*PERMITTED_PATHS.toTypedArray<String>())
private String clientSecret; .permitAll()
.and()
.authorizeHttpRequests()
@Bean .requestMatchers(HttpMethod.OPTIONS)
public SecurityFilterChain securityFilterChain(@NonNull HttpSecurity http) throws Exception { .permitAll()
http .and()
.csrf().disable() .authorizeHttpRequests()
.authorizeHttpRequests() .anyRequest()
.requestMatchers(PERMITTED_PATHS.toArray(new String[0])) .authenticated()
.permitAll() .and()
.and() .oauth2ResourceServer { obj: OAuth2ResourceServerConfigurer<HttpSecurity?> -> obj.jwt() }
.authorizeHttpRequests() .addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter::class.java)
.requestMatchers(HttpMethod.OPTIONS) return http.build()
.permitAll()
.and()
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter.class);
return http.build();
}
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
}
private SpringPolicyEnforcerFilter createPolicyEnforcerFilter() {
return new SpringPolicyEnforcerFilter(new ConfigurationResolver() {
@Override
public PolicyEnforcerConfig resolve(HttpRequest request) {
try {
PolicyEnforcerConfig policyEnforcerConfig = JsonSerialization.readValue(
getClass().getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig.class);
policyEnforcerConfig.setCredentials(Map.of("secret", clientSecret));
if (request.getMethod().equals(HttpMethod.OPTIONS.name())) {
// always allow options request
policyEnforcerConfig.setEnforcementMode(EnforcementMode.DISABLED);
} else {
policyEnforcerConfig.setPaths(PATHS);
}
return policyEnforcerConfig;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
@NonNull
private static List<PathConfig> buildPathConfigs() {
List<PathConfig> paths = new ArrayList<>();
for (String path : PERMITTED_PATHS) {
PathConfig pathConfig = new PathConfig();
pathConfig.setPath(path.replace("**", "*"));
pathConfig.setEnforcementMode(EnforcementMode.DISABLED);
paths.add(pathConfig);
} }
return paths;
}
@Bean
open fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
private fun createPolicyEnforcerFilter(): SpringPolicyEnforcerFilter {
return SpringPolicyEnforcerFilter(object : ConfigurationResolver {
override fun resolve(request: HttpRequest): PolicyEnforcerConfig {
return try {
val policyEnforcerConfig = JsonSerialization.readValue(
javaClass.getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig::class.java
)
policyEnforcerConfig.credentials = mapOf(Pair("secret", clientSecret))
if (request.method == HttpMethod.OPTIONS.name()) {
// always allow options request
policyEnforcerConfig.enforcementMode = EnforcementMode.DISABLED
} else {
policyEnforcerConfig.paths = PATHS
}
policyEnforcerConfig
} catch (e: IOException) {
throw RuntimeException(e)
}
}
})
}
companion object {
private val PERMITTED_PATHS: Collection<String> = listOf(
"/wahlrecht/v1/healthCheck",
"/actuator/**",
"/wahlrecht/v1/doc/**",
"/wahlrecht/v1/api-docs/**",
"/error"
)
private val PATHS = buildPathConfigs()
private fun buildPathConfigs(): List<PathConfig> {
val paths: MutableList<PathConfig> = mutableListOf()
for (path in PERMITTED_PATHS) {
val pathConfig = PathConfig()
pathConfig.path = path.replace("**", "*")
pathConfig.enforcementMode = EnforcementMode.DISABLED
paths.add(pathConfig)
}
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(
ElectedCandidatesMapper::class.java
)
private val electionResultMapper = Mappers.getMapper(
ElectionResultMapper::class.java
)
private final ElectedCandidatesMapper electedCandidatesMapper = Mappers.getMapper( @Operation(
ElectedCandidatesMapper.class); summary = "Calculates a provided election result",
private final ElectionResultMapper electionResultMapper = Mappers.getMapper( description = "This request does not store any result and is idempotent.",
ElectionResultMapper.class); responses = [ApiResponse(
private final CalculationService service; responseCode = "200",
description = "Returns all elected candidates",
@Operation( content = [Content(
summary = "Calculates a provided election result", mediaType = "application/json",
description = "This request does not store any result and is idempotent.", schema = Schema(implementation = ElectedCandidates::class)
responses = { )]
@ApiResponse(responseCode = "200", )]
description = "Returns all elected candidates", )
content = {@Content( @PostMapping(value = ["/calculate"], produces = [MediaType.APPLICATION_JSON_VALUE])
mediaType = "application/json", @SecurityRequirement(name = "bearerAuth")
schema = @Schema(implementation = ElectedCandidates.class))} @SecurityRequirement(name = "oauth2")
) fun calculateResult(
} @RequestBody electionResult: ElectionResult
) ): ResponseEntity<ElectedCandidates> {
@PostMapping(value = "/calculate", produces = MediaType.APPLICATION_JSON_VALUE) val result = electedCandidatesMapper.mapToExternal(
@SecurityRequirement(name = "bearerAuth") service.determineElectedCandidates(electionResultMapper.mapToInternal(electionResult))
@SecurityRequirement(name = "oauth2") )
public ResponseEntity<ElectedCandidates> calculateResult( return ResponseEntity.ok()
@RequestBody ElectionResult electionResult .contentType(MediaType.APPLICATION_JSON)
) { .body(result)
ElectedCandidates result = electedCandidatesMapper.mapToExternal( }
service.determineElectedCandidates(electionResultMapper.mapToInternal(electionResult)));
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.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); @SecurityRequirement(name = "oauth2")
private final ElectionService service; @SecurityRequirement(name = "bearerAuth")
@GetMapping(value = ["/elections"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation(
summary = "Returns all stored elections",
responses = [ApiResponse(
responseCode = "200",
description = "Returns all stored elections",
content = [Content(
mediaType = "application/json",
array = ArraySchema(
schema = Schema(
implementation = Election::class
)
)
)]
)]
)
fun getElections(): ResponseEntity<Collection<Election>> {
val elections = service.elections.asSequence()
.map { mapper.mapToExternal(it.value) }
.toList()
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(elections)
}
@Operation( @Operation(
summary = "Returns all stored elections", summary = "Returns election matching provided name",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "200",
description = "Returns all stored elections", description = "Returns matching election",
content = {@Content( content = [Content(mediaType = "application/json", schema = Schema(implementation = Election::class))]
mediaType = "application/json", )]
array = @ArraySchema(schema = @Schema(implementation = Election.class)))} )
) @GetMapping(value = ["/elections/by-name/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
} @SecurityRequirement(name = "bearerAuth")
) @SecurityRequirement(name = "oauth2")
@GetMapping(value = "/elections", produces = MediaType.APPLICATION_JSON_VALUE) fun getElectionByName(
@SecurityRequirement(name = "bearerAuth") @PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String?
@SecurityRequirement(name = "oauth2") ): ResponseEntity<Election> {
public ResponseEntity<Collection<Election>> getElections() { val election = mapper.mapToExternal(service.elections[electionName]!!)
List<Election> elections = service.getElections().stream().map(mapper::mapToExternal).toList(); return ResponseEntity.ok()
return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON) .body(election)
.body(elections); }
}
@Operation( @Operation(
summary = "Returns election matching provided name", summary = "Stores a new election",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "201",
description = "Returns matching election", description = "Election was created"
content = {@Content( ), ApiResponse(responseCode = "200", description = "Election was modified"), ApiResponse(
mediaType = "application/json", responseCode = "401",
schema = @Schema(implementation = Election.class))} description = "Unauthorized",
) content = [Content(mediaType = "plain/text")]
} )]
) )
@GetMapping(value = "/elections/by-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE) @PutMapping("/election")
@SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2") @SecurityRequirement(name = "oauth2")
public ResponseEntity<Election> getElectionByName( fun putElection(@RequestBody election: Election?): ResponseEntity<*> {
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) { val createdNew = service.storeElection(mapper.mapToDB(election!!))
Election election = mapper.mapToExternal(service.getElection(electionName)); return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK)
return ResponseEntity.ok() }
.contentType(MediaType.APPLICATION_JSON)
.body(election);
}
@Operation(
summary = "Stores a new election",
responses = {
@ApiResponse(responseCode = "201",
description = "Election was created"),
@ApiResponse(responseCode = "200",
description = "Election was modified"),
@ApiResponse(responseCode = "401",
description = "Unauthorized",
content = {@Content(
mediaType = "plain/text")}
)
}
)
@PutMapping("/election")
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
public ResponseEntity<?> putElection(@RequestBody Election election) {
boolean createdNew = service.storeElection(mapper.mapToDB(election));
return createdNew
? 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); @Operation(
private final ElectionResultService service; summary = "Returns election result for given election name",
responses = [ApiResponse(
responseCode = "200",
description = "Returns stored election result for given election name",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ElectionResult::class))]
), ApiResponse(
responseCode = "404",
description = "No election result found",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorMessage::class))]
)]
)
@GetMapping(
value = ["/electionResult/by-election-name/{electionName}"],
produces = [MediaType.APPLICATION_JSON_VALUE]
)
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun getElectionByName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String
): ResponseEntity<ElectionResult> {
val result = mapper.mapToExternal(service.getElectionResult(electionName))
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(result)
}
@Operation( @Operation(
summary = "Returns election result for given election name", summary = "Stores a new election",
responses = { responses = [ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "201",
description = "Returns stored election result for given election name", description = "Election was created"
content = {@Content( ), ApiResponse(responseCode = "200", description = "Election was modified"), ApiResponse(
mediaType = "application/json", responseCode = "401",
schema = @Schema(implementation = ElectionResult.class))} description = "Unauthorized",
), content = [Content(mediaType = "plain/text")]
@ApiResponse(responseCode = "404", )]
description = "No election result found", )
content = { @PutMapping("/electionResult")
@Content( @SecurityRequirement(name = "bearerAuth")
mediaType = "application/json", @SecurityRequirement(name = "oauth2")
schema = @Schema(implementation = ErrorMessage.class) fun putElection(@RequestBody electionResult: ElectionResult?): ResponseEntity<*> {
) val createdNew = service.storeResult(mapper.mapToDB(electionResult!!))
} return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK)
) }
}
)
@GetMapping(value = "/electionResult/by-election-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
public ResponseEntity<ElectionResult> getElectionByName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) {
ElectionResult result = mapper.mapToExternal(service.getElectionResult(electionName));
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(result);
}
@Operation(
summary = "Stores a new election",
responses = {
@ApiResponse(responseCode = "201",
description = "Election was created"),
@ApiResponse(responseCode = "200",
description = "Election was modified"),
@ApiResponse(responseCode = "401",
description = "Unauthorized",
content = {@Content(
mediaType = "plain/text")}
)
}
)
@PutMapping("/electionResult")
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
public ResponseEntity<?> putElection(@RequestBody ElectionResult electionResult) {
boolean createdNew = service.storeResult(mapper.mapToDB(electionResult));
return createdNew
? 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)
@Operation(
summary = "Returns all stored parties for given election name",
responses = [ApiResponse(
responseCode = "200",
description = "Returns all stored parties",
content = [Content(
mediaType = "application/json",
array = ArraySchema(schema = Schema(implementation = PartyInElection::class))
)]
), ApiResponse(
responseCode = "404",
description = "No parties found",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorMessage::class))]
)]
)
@GetMapping(value = ["/parties/by-election-name/{electionName}"], produces = [MediaType.APPLICATION_JSON_VALUE])
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun getPartiesByElectionName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") electionName: String
): ResponseEntity<Collection<PartyInElection>> {
val parties = service.getPartiesByElectionName(electionName).stream()
.map { election: de.twomartens.wahlrecht.model.db.PartyInElection? ->
mapper.mapToExternal(
election!!
)
}
.toList()
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(parties)
}
private final PartyInElectionMapper mapper = Mappers.getMapper(PartyInElectionMapper.class); @Operation(
private final PartyService service; summary = "Stores a new party in an election",
responses = [ApiResponse(
@Operation( responseCode = "201",
summary = "Returns all stored parties for given election name", description = "Party was created"
responses = { ), ApiResponse(responseCode = "200", description = "Party was modified"), ApiResponse(
@ApiResponse(responseCode = "200", responseCode = "401",
description = "Returns all stored parties", description = "Unauthorized"
content = {@Content( )]
mediaType = "application/json", )
array = @ArraySchema(schema = @Schema(implementation = PartyInElection.class)))} @PutMapping("/party")
), @SecurityRequirement(name = "bearerAuth")
@ApiResponse(responseCode = "404", @SecurityRequirement(name = "oauth2")
description = "No parties found", fun putParty(@RequestBody party: PartyInElection?): ResponseEntity<*> {
content = { val createdNew = service.storeParty(mapper.mapToDB(party!!))
@Content( return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK)
mediaType = "application/json", }
schema = @Schema(implementation = ErrorMessage.class)
)
}
)
}
)
@GetMapping(value = "/parties/by-election-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
public ResponseEntity<Collection<PartyInElection>> getPartiesByElectionName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) {
List<PartyInElection> parties = service.getPartiesByElectionName(electionName).stream()
.map(mapper::mapToExternal)
.toList();
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(parties);
}
@Operation(
summary = "Stores a new party in an election",
responses = {
@ApiResponse(responseCode = "201",
description = "Party was created"),
@ApiResponse(responseCode = "200",
description = "Party was modified"),
@ApiResponse(responseCode = "401",
description = "Unauthorized"
)
}
)
@PutMapping("/party")
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
public ResponseEntity<?> putParty(@RequestBody PartyInElection party) {
boolean createdNew = service.storeParty(mapper.mapToDB(party));
return createdNew
? 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 @Id
@FieldDefaults(level = AccessLevel.PRIVATE) var id: ObjectId = ObjectId()
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Candidate {
@EqualsAndHashCode.Exclude @CreatedDate
@Id lateinit var created: Instant
ObjectId id;
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
@Indexed @LastModifiedDate
String name; lateinit var lastModified: Instant
String profession;
} }
@@ -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,
var electionName: String,
var name: String,
var numberOfSeats: Int = 0
) {
@Id
var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude @CreatedDate
@Id lateinit var created: Instant
ObjectId id;
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
int number; @LastModifiedDate
String electionName; lateinit var lastModified: Instant
String name;
int numberOfSeats;
} }
@@ -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) @Id
public class Election { var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude @CreatedDate
@Id lateinit var created: Instant
ObjectId id;
@EqualsAndHashCode.Exclude @LastModifiedDate
@CreatedDate lateinit var lastModified: Instant
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
@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) @Id
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) var id: ObjectId = ObjectId()
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ElectionResult {
@EqualsAndHashCode.Exclude
@Id
ObjectId id;
@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,
var electionName: String,
var name: String,
var overallNomination: Nomination,
var constituencyNominations: Map<Int, Nomination?>
) {
@Id
var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude @EqualsAndHashCode.Exclude
@Id @CreatedDate
ObjectId id; lateinit var created: Instant
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
String abbreviation; @EqualsAndHashCode.Exclude
String electionName; @LastModifiedDate
String name; lateinit var lastModified: Instant
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,8 +1,8 @@
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,
OVERALL_VOTE_ORDER OVERALL_VOTE_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 {
try (Closeable ignored = preparable.prepare()) { preparable.prepare().use {
Health result = null; var result: Health? = null
Exception exception = null; var exception: Exception? = null
long start = clock.millis(); val start = clock.millis()
try { try {
result = determineHealth(); result = determineHealth()
} catch (RuntimeException e) { } catch (e: RuntimeException) {
exception = e; exception = e
result = Health.down().withException(e).build(); 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)
}
@Override return healthBuilder.build()
protected Health determineHealth() { }
Builder healthBuilder = Health.status(statusProbe.getStatus());
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();
}
companion object {
const val MESSAGE_KEY = "message"
const val LAST_STATUS_CHANGE_KEY = "lastStatusChange"
}
} }
@@ -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,
serverProperties: ServerProperties, private val restTemplateRestHealthIndicator: RestTemplate
) : AbstractHealthIndicator(clock, Preparable { interceptor.markAsHealthCheck() }), HealthIndicator {
private val randomizer = SecureRandom()
private val urlPrefix: String = (HTTP_PREFIX + HOST + HOST_PORT_SEPARATOR
+ serverProperties.port
+ URL_PATH + PARAMETER_SEPARATOR + GET_PARAMETER)
private static final String URL_PATH = "/wahlrecht/v1/healthCheck"; /**
private static final String GET_PARAMETER = "message="; * main method that determines the health of the service
*/
private final SecureRandom randomizer = new SecureRandom(); override fun determineHealth(): Health {
private final RestTemplate restTemplateRestHealthIndicator; val random = randomizer.nextInt(100000, 999999).toString()
private final String urlPrefix; val url = "$urlPrefix{random}"
val response = restTemplateRestHealthIndicator.getForEntity(url, String::class.java, random)
public RestHealthIndicator(Clock clock, HeaderInterceptorRest interceptor, val status = if (response.body == random) Status.UP else Status.DOWN
ServerProperties serverProperties, RestTemplate restTemplateRestHealthIndicator) { return Health.status(status)
super(clock, interceptor::markAsHealthCheck); .withDetail(DETAIL_ENDPOINT_KEY, url)
this.restTemplateRestHealthIndicator = restTemplateRestHealthIndicator; .build()
urlPrefix = HTTP_PREFIX + HOST + HOST_PORT_SEPERATOR + serverProperties.getPort() }
+ URL_PATH + PARAMETER_SEPERATOR + GET_PARAMETER;
}
/**
* main method that determines the health of the service
*/
@Override
protected Health determineHealth() {
String random = Integer.toString(randomizer.nextInt(100000, 999999));
String url = urlPrefix + "{random}";
ResponseEntity<String> response = restTemplateRestHealthIndicator.getForEntity(url, String.class, random);
Status status = Optional.ofNullable(response.getBody())
.filter(random::equals)
.map(m -> Status.UP)
.orElse(Status.DOWN);
return Health.status(status)
.withDetail(DETAIL_ENDPOINT_KEY, url)
.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, if (failureCount > maxFailureCount) {
StatusProbeLogger statusProbeLogger) { super.setStatus(status, throwable, message)
super(clock, criticality, name, statusProbeLogger); }
this.maxFailureCount = maxFailureCount; } else if (status === Status.UP) {
} failureCount.set(0)
super.setStatus(status, throwable, message)
@Override }
protected synchronized void setStatus(Status status, Throwable throwable, String message) {
if (status == Status.DOWN) {
int failureCount = this.failureCount.incrementAndGet();
if (failureCount > maxFailureCount) {
super.setStatus(status, throwable, message);
}
} else if (status == Status.UP) {
this.failureCount.set(0);
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,
ThreadPoolTaskScheduler threadPoolTaskScheduler, Duration schedulePeriod, StatusProbeCriticality criticality,
String name, StatusProbeLogger statusProbeLogger) {
super(clock, criticality, name, statusProbeLogger);
this.maxFailurePercent = maxFailurePercent;
scheduleTask(threadPoolTaskScheduler, schedulePeriod);
}
@Override
protected synchronized void setStatus(Status status, Throwable throwable, String message) {
if (status == Status.DOWN) {
downCount++;
this.throwable = throwable;
this.message = message;
} }
requestCount++;
}
private void reset() { @Synchronized
requestCount = 0; override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
downCount = 0; if (status === Status.DOWN) {
throwable = null; downCount++
message = null; this.temporaryThrowable = throwable
} this.temporaryMessage = message
}
public synchronized void runScheduledTask() { requestCount++
if (requestCount > 0 && (downCount * 100.0 / requestCount) > maxFailurePercent) {
super.setStatus(Status.DOWN, throwable, message);
} else {
super.setStatus(Status.UP, null, null);
} }
reset();
}
private fun reset() {
requestCount = 0
downCount = 0
}
@Synchronized
override fun runScheduledTask() {
if (requestCount > 0 && downCount * 100.0 / requestCount > maxFailurePercent) {
super.setStatus(Status.DOWN, temporaryThrowable, temporaryMessage)
} else {
super.setStatus(Status.UP, null, null)
}
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 {
void runScheduledTask();
default void scheduleTask(ThreadPoolTaskScheduler threadPoolTaskScheduler,
Duration schedulePeriod) {
PeriodicTrigger periodicTrigger = new PeriodicTrigger(
Duration.ofSeconds(schedulePeriod.toSeconds()));
threadPoolTaskScheduler.schedule(this::runScheduledTask, periodicTrigger);
}
interface ScheduledStatusProbe {
fun runScheduledTask()
fun scheduleTask(
threadPoolTaskScheduler: ThreadPoolTaskScheduler,
schedulePeriod: Duration
) {
val periodicTrigger = PeriodicTrigger(
Duration.ofSeconds(schedulePeriod.toSeconds())
)
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) {
if (status != this.status) {
lastStatusChange = ZonedDateTime.now(clock);
statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable);
} }
this.status = status;
this.throwable = throwable;
this.message = message;
}
public void up() { protected open fun setStatus(status: Status, throwable: Throwable?, message: String?) {
setStatus(Status.UP, null, null); if (status !== this.status) {
} lastStatusChange = ZonedDateTime.now(clock)
statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable)
}
this.status = status
this.throwable = throwable
this.message = message
}
public void up(String message) { fun up() {
setStatus(Status.UP, null, message); setStatus(Status.UP, null, null)
} }
public void down() { fun up(message: String) {
setStatus(Status.DOWN, null, null); setStatus(Status.UP, null, message)
} }
public void down(Throwable throwable) { fun down() {
setStatus(Status.DOWN, throwable, null); setStatus(Status.DOWN, null, null)
} }
public void down(String message) { fun down(throwable: Throwable) {
setStatus(Status.DOWN, null, message); setStatus(Status.DOWN, throwable, null)
} }
protected void down(Throwable throwable, String message) { fun down(message: String) {
setStatus(Status.DOWN, throwable, message); setStatus(Status.DOWN, null, message)
} }
protected fun down(throwable: Throwable, message: String) {
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"));
}
/**
* only for testing purposes
*/
StatusProbeLogger(Clock clock, Logger commLog) {
this.clock = clock;
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) {
probeIdent = new ProbeIdent(name, K1);
} }
statusProbeToStatus.put(probeIdent, status);
createLog(message, lastStatusChange, throwable);
}
private ProbeIdent getProbeIdent(String name) { fun logStatusChange(
return statusProbeToStatus.keySet().stream().filter(key -> key.name.equals(name)).findFirst().orElse(null); name: String, message: String?, status: Status, lastStatusChange: ZonedDateTime?,
} throwable: Throwable?
) {
private void createLog(String message, ZonedDateTime lastStatusChange, Throwable throwable) { var probeIdent = getProbeIdent(name)
Status overallStatus = getOverallStatus(); if (probeIdent == null) {
StatusProbeCriticality criticality = getOverallCriticality(); probeIdent = ProbeIdent(name, StatusProbeCriticality.K1)
if (message == null) { }
message = ""; statusProbeToStatus[probeIdent] = status
createLog(message, lastStatusChange, throwable)
} }
if (Status.UP.equals(overallStatus)) {
commLog.info(MARKER, new StringMapMessage() private fun getProbeIdent(name: String): ProbeIdent? {
.with(LABEL_CRITICALITY, criticality) return statusProbeToStatus.keys.asSequence()
.with(LABEL_STATUS, overallStatus) .filter { it.name == name }
.with(LABEL_MESSAGE, message) .firstOrNull()
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange));
} else {
commLog.error(MARKER, new StringMapMessage()
.with(LABEL_CRITICALITY, criticality)
.with(LABEL_STATUS, overallStatus)
.with(LABEL_MESSAGE, message)
.with(LABEL_REASON, getReason())
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable);
} }
}
private StatusProbeCriticality getOverallCriticality() { private fun createLog(message: String?, lastStatusChange: ZonedDateTime?, throwable: Throwable?) {
List<StatusProbeCriticality> crits = statusProbeToStatus.keySet().stream().map(key -> key.criticality).toList(); val cleanedMessage = message ?: ""
return crits.contains(K1) ? K1 : crits.contains(K2) ? K2 : K3; val overallStatus = overallStatus
} val criticality = overallCriticality
if (Status.UP == overallStatus) {
private Status getOverallStatus() { commLog.info(
if (statusProbeToStatus.containsValue(Status.DOWN)) { MARKER, StringMapMessage()
return Status.DOWN; .with(LABEL_CRITICALITY, criticality)
.with(LABEL_STATUS, overallStatus)
.with(LABEL_MESSAGE, cleanedMessage)
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange)
)
} else {
commLog.error(
MARKER, StringMapMessage()
.with(LABEL_CRITICALITY, criticality)
.with(LABEL_STATUS, overallStatus)
.with(LABEL_MESSAGE, cleanedMessage)
.with(LABEL_REASON, reason)
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable
)
}
} }
return Status.UP;
}
private String getReason() { private val overallCriticality: StatusProbeCriticality
get() {
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
}
List<ProbeIdent> probesDown = statusProbeToStatus.entrySet().stream() private val overallStatus: Status
.filter(entry -> Status.DOWN.equals(entry.getValue())) get() = if (statusProbeToStatus.containsValue(Status.DOWN)) Status.DOWN
.map(Map.Entry::getKey) else Status.UP
.toList();
String reasonK1 = getDownStatusProbes(probesDown, K1); private val reason: String
String reasonK2 = getDownStatusProbes(probesDown, K2); get() {
String reasonK3 = getDownStatusProbes(probesDown, K3); 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 <= ' ' }
}
return "%s%s%s".formatted(reasonK1, reasonK2, reasonK3).trim(); private fun getDownStatusProbes(probesDown: List<ProbeIdent>, criticality: StatusProbeCriticality): String {
} val downProbeNames = probesDown.asSequence()
.filter { it.criticality == criticality }
private String getDownStatusProbes(List<ProbeIdent> probesDown, StatusProbeCriticality criticality) { .map { it.name }
List<String> downProbeNames = probesDown.stream().filter(probe -> probe.criticality.equals(criticality)) .toList()
.map(probe -> probe.name).toList(); return if (downProbeNames.isNotEmpty()) {
if (downProbeNames.size() > 0) { "$criticality failed: ${downProbeNames.joinToString(separator = ",")}\n"
return criticality + " failed: " + String.join(",", downProbeNames) + "\n"; } else ""
} }
return "";
}
public record ProbeIdent(String name, StatusProbeCriticality criticality) { data class ProbeIdent(val name: String, val criticality: StatusProbeCriticality)
} 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,
ThreadPoolTaskScheduler threadPoolTaskScheduler, Duration schedulePeriod, StatusProbeCriticality criticality,
String name, StatusProbeLogger statusProbeLogger) {
super(clock, criticality, name, statusProbeLogger);
this.clock = clock;
this.maxFailureDuration = maxFailureDuration;
this.lastSuccess = null;
scheduleTask(threadPoolTaskScheduler, schedulePeriod);
}
@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() { @Synchronized
if (lastSuccess == null) { override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
return false; if (status === Status.DOWN) {
this.temporaryThrowable = throwable
this.temporaryMessage = message
} else if (status === Status.UP) {
lastSuccess = ZonedDateTime.now(clock)
super.setStatus(status, throwable, message)
}
} }
Duration timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock));
return maxFailureDuration.minus(timeSinceLastSuccess).isNegative();
}
public synchronized void runScheduledTask() { private val isOverdue: Boolean
if (isOverdue()) { get() {
super.setStatus(Status.DOWN, throwable, message); if (lastSuccess == null) {
return false
}
val timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock))
return maxFailureDuration.minus(timeSinceLastSuccess).isNegative
}
@Synchronized
override fun runScheduledTask() {
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;
} }
if (existing != null) { fun storeCandidate(candidate: Candidate): Candidate {
existing.setProfession(candidate.getProfession()); var result = candidate
candidate = existing; val existing = candidates[candidate.name]
val needsUpdate = candidate != existing
if (!needsUpdate && existing != null) {
return existing
}
if (existing != null) {
existing.profession = candidate.profession
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) {
existing.numberOfSeats = constituency.numberOfSeats
existing.name = constituency.name
result = existing
}
val stored = repository.save(result)
constituencies[constituencyId] = stored
return stored
} }
if (existing != null) { private fun fetchConstituencies(): MutableMap<ConstituencyId, Constituency> {
existing.setNumberOfSeats(constituency.getNumberOfSeats()); return repository.findAll().asSequence()
existing.setName(constituency.getName()); .map { Pair(ConstituencyId(electionName = it.electionName, number = it.number), it) }
constituency = existing; .toMap()
.toMutableMap()
} }
Constituency stored = repository.save(constituency);
constituencies.put(constituencyId, stored);
return stored;
}
private void fetchConstituencies() {
constituencies = repository.findAll().stream()
.collect(Collectors.toMap(
constituency -> new ConstituencyId(constituency.getElectionName(),
constituency.getNumber()),
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) {
if (electionResults == null) {
fetchResults();
} }
String id = result.getElectionName(); fun storeResult(electionResult: ElectionResult): Boolean {
ElectionResult existing = electionResults.get(id); var result = electionResult
boolean needsUpdate = !result.equals(existing);
if (!needsUpdate) { val id = result.electionName
return false; val existing = electionResults[id]
val needsUpdate = result != existing
if (!needsUpdate) {
return false
}
var createdNew = true
if (existing != null) {
existing.overallResults = result.overallResults
existing.constituencyResults = result.constituencyResults
result = existing
createdNew = false
}
val stored = repository.save(result)
electionResults[id] = stored
return createdNew
} }
boolean createdNew = true; private fun fetchResults(): MutableMap<String, ElectionResult> {
if (existing != null) { return repository.findAll().asSequence()
existing.setOverallResults(result.getOverallResults()); .map { Pair(it.electionName, it) }
existing.setConstituencyResults(result.getConstituencyResults()); .toMap()
result = existing; .toMutableMap()
createdNew = false;
} }
ElectionResult stored = repository.save(result);
electionResults.put(id, stored);
return createdNew;
}
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) {
if (elections == null) {
fetchElections();
}
return elections.get(electionName);
}
public de.twomartens.wahlrecht.model.internal.Election getElectionInternal(String electionName) {
return electionMapper.mapToInternal(getElection(electionName));
}
public boolean storeElection(@NonNull Election election) {
if (elections == null) {
fetchElections();
}
boolean createdNew = true;
String electionName = election.getName();
Election existing = elections.get(electionName);
if (election.equals(existing)) {
return false;
} }
Collection<Constituency> constituencies = new ArrayList<>(); val elections: Map<String, Election> = _elections
election.getConstituencies()
.forEach(constituency -> constituencies.add(
constituencyService.storeConstituency(constituency)));
if (existing != null) { private fun getElection(electionName: String): Election? {
existing.setDay(election.getDay()); return elections[electionName]
existing.setTotalNumberOfSeats(election.getTotalNumberOfSeats());
existing.setVotingThreshold(election.getVotingThreshold());
election = existing;
createdNew = false;
} }
election.setConstituencies(constituencies); fun getElectionInternal(electionName: String): de.twomartens.wahlrecht.model.internal.Election {
Election stored = electionRepository.save(election); return electionMapper.mapToInternal(getElection(electionName)!!)
elections.put(electionName, stored); }
return createdNew; fun storeElection(election: Election): Boolean {
} var result = election
var createdNew = true
val electionName = election.name
val existing = elections[electionName]
if (election == existing) {
return false
}
val constituencies: MutableCollection<Constituency> = mutableListOf()
result.constituencies
.forEach { constituencies.add(constituencyService.storeConstituency(it)) }
if (existing != null) {
existing.day = election.day
existing.totalNumberOfSeats = election.totalNumberOfSeats
existing.votingThreshold = election.votingThreshold
result = existing
createdNew = false
}
result.constituencies = constituencies
val stored = electionRepository.save(result)
_elections[electionName] = stored
return createdNew
}
private void fetchElections() { private fun fetchElections(): MutableMap<String, Election> {
elections = electionRepository.findAll().stream() return electionRepository.findAll().asSequence()
.collect(Collectors.toMap(Election::getName, Function.identity())); .map { Pair(it.name, it) }
} .toMap()
.toMutableMap()
}
} }
@@ -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) {
Collection<PartyInElection> parties = partyRepository.findByElectionName(electionName);
if (parties.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "no party found for " + electionName);
} }
return parties; fun getPartiesByElectionName(electionName: String): Collection<PartyInElection> {
} val parties = partyRepository.findByElectionName(electionName)
if (parties.isEmpty()) {
public boolean storeParty(PartyInElection partyInElection) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "no party found for $electionName")
if (parties == null) { }
fetchParties(); return parties
} }
boolean createdNew = true; fun storeParty(partyInElection: PartyInElection): Boolean {
PartyId partyId = new PartyId(partyInElection.getElectionName(), var result = partyInElection
partyInElection.getAbbreviation()); var createdNew = true
PartyInElection existing = parties.get(partyId); val partyId = PartyId(
boolean needsUpdate = !partyInElection.equals(existing); partyInElection.electionName,
if (!needsUpdate) { partyInElection.abbreviation
return false; )
val existing = parties[partyId]
val needsUpdate = partyInElection != existing
if (!needsUpdate) {
return false
}
val constituencyNominations: MutableMap<Int, Nomination?> = mutableMapOf()
result.constituencyNominations
.forEach { constituencyNominations[it.key] = nominationService.storeNomination(it.value) }
if (existing != null) {
existing.name = partyInElection.name
if (constituencyNominations.isEmpty()) {
constituencyNominations.putAll(existing.constituencyNominations)
}
result = existing
createdNew = false
}
partyInElection.constituencyNominations = constituencyNominations
partyInElection.overallNomination = nominationService.storeNomination(partyInElection.overallNomination)!!
result = partyRepository.save(result)
parties[partyId] = result
return createdNew
} }
Map<Integer, Nomination> constituencyNominations = new HashMap<>(); private fun fetchParties(): MutableMap<PartyId, PartyInElection> {
partyInElection.getConstituencyNominations() val allParties = partyRepository.findAll()
.forEach((key, value) -> constituencyNominations.put(key, return allParties.asSequence()
nominationService.storeNomination(value))); .map { Pair(PartyId(electionName = it.electionName, abbreviation = it.abbreviation), it) }
if (existing != null) { .toMap()
existing.setName(partyInElection.getName()); .toMutableMap()
if (constituencyNominations.isEmpty()) {
constituencyNominations.putAll(existing.getConstituencyNominations());
}
partyInElection = existing;
createdNew = false;
} }
partyInElection.setConstituencyNominations(constituencyNominations);
partyInElection.setOverallNomination(
nominationService.storeNomination(partyInElection.getOverallNomination()));
partyInElection = partyRepository.save(partyInElection);
parties.put(partyId, partyInElection);
return createdNew;
}
private void fetchParties() {
List<PartyInElection> allParties = partyRepository.findAll();
parties = allParties.stream()
.collect(Collectors.toMap(
p -> new PartyId(p.getElectionName(), p.getAbbreviation()),
Function.identity()));
}
} }
@@ -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(); .with(PARAM_MESSAGE, "properties changed")
if (!properties.isEmpty()) { .with(PARAM_PROPERTIES, properties.joinToString("\n"))
log.info(new StringMapMessage() .asString()
.with(PARAM_MESSAGE, "properties changed") )
.with(PARAM_PROPERTIES, String.join("\n", properties))); }
} }
}
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,62 +444,63 @@ 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(
GRUENE_BEZIRK_RESULT.nominationId.partyAbbreviation, listOf( Pair(
ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY), GRUENE_BEZIRK_RESULT.nominationId.partyAbbreviation, listOf(
ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(3), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(5), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(3), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(6), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(5), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(7), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(6), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(11), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(7), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(15), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(11), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(16), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(15), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(20), Elected.CONSTITUENCY), ElectedCandidate(GRUENE_BEZIRK.getCandidate(16), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(22), Elected.CONSTITUENCY) ElectedCandidate(GRUENE_BEZIRK.getCandidate(20), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(22), Elected.CONSTITUENCY)
)
) )
) )
} }