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 java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
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 {
doFirst {
@@ -59,7 +59,7 @@ tasks.jar {
attributes["Implementation-Title"] = rootProject.name
attributes["Implementation-Version"] = archiveVersion.get()
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["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()}"
@@ -12,7 +12,6 @@ dependencies {
implementation(libs.bundles.spring.boot)
testImplementation(libs.spring.boot.test)
annotationProcessor(libs.spring.boot.config)
}
sourceSets {
@@ -31,7 +30,7 @@ idea {
}
}
val integrationTestImplementation by configurations.getting {
val integrationTestImplementation: Configuration by configurations.getting {
extendsFrom(configurations.testImplementation.get())
}
@@ -70,3 +69,15 @@ tasks.jar {
enabled = true
archiveClassifier.set("")
}
tasks.bootDistZip {
dependsOn(tasks.jar)
}
tasks.bootDistTar {
dependsOn(tasks.jar)
}
tasks.bootStartScripts {
dependsOn(tasks.jar)
}
+1
View File
@@ -7,3 +7,4 @@ org.gradle.daemon=true
org.gradle.welcome=never
org.gradle.caching=true
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.kotlin")
kotlin("kapt")
alias(libs.plugins.kotlin.lombok)
}
dependencies {
@@ -11,7 +10,3 @@ dependencies {
annotationProcessor(libs.mapstruct.processor)
kapt(libs.mapstruct.processor)
}
kapt {
keepJavacAnnotationProcessors = true
}
@@ -6,7 +6,7 @@ import org.springframework.context.annotation.Configuration
import java.time.Clock
@Configuration
open class ClockConfiguration(val serviceProperties: ServiceProperties) {
open class ClockConfiguration(private val serviceProperties: ServiceProperties) {
@Bean
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.security.OAuthFlow;
import io.swagger.v3.oas.annotations.security.OAuthFlows;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.OpenAPI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.OAuthFlow
import io.swagger.v3.oas.annotations.security.OAuthFlows
import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT"
)
@SecurityScheme(name = "bearerAuth", type = SecuritySchemeType.HTTP, scheme = "bearer", bearerFormat = "JWT")
@SecurityScheme(
name = "oauth2",
type = SecuritySchemeType.OAUTH2,
flows = @OAuthFlows(
implicit = @OAuthFlow(
flows = OAuthFlows(
implicit = OAuthFlow(
authorizationUrl = "https://id.2martens.de/realms/2martens/protocol/openid-connect/auth",
tokenUrl = "https://id.2martens.de/realms/2martens/protocol/openid-connect/token"
)
)
)
@Configuration
public class OpenApiConfiguration {
@Bean
public OpenAPI customOpenAPI(@Value("${openapi.description}") String apiDesciption,
@Value("${openapi.version}") String apiVersion, @Value("${openapi.title}") String apiTitle) {
return new OpenAPI()
.info(new io.swagger.v3.oas.models.info.Info()
.title(apiTitle)
.version(apiVersion)
.description(apiDesciption));
}
open class OpenApiConfiguration {
@Bean
open fun customOpenAPI(
@Value("\${openapi.description}") apiDescription: String,
@Value("\${openapi.version}") apiVersion: String, @Value("\${openapi.title}") apiTitle: String
): OpenAPI {
return OpenAPI()
.info(
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.StatusProbe;
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality;
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeLogger;
import java.time.Clock;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import de.twomartens.wahlrecht.monitoring.statusprobe.CountBasedStatusProbe
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbe
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeLogger
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
@Configuration
@RequiredArgsConstructor
public class StatusProbeConfiguration {
open class StatusProbeConfiguration(private val clock: Clock) {
@Bean
open fun statusProbeLogger(): StatusProbeLogger {
return StatusProbeLogger(clock)
}
private final Clock clock;
@Bean
public StatusProbeLogger statusProbeLogger() {
return new StatusProbeLogger(clock);
}
@Bean
public StatusProbe testStatusProbe(StatusProbeLogger statusProbeLogger) {
return new CountBasedStatusProbe(1,
clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger);
}
@Bean
open fun testStatusProbe(statusProbeLogger: StatusProbeLogger): StatusProbe {
return 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 lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.CorsRegistration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
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
@RequiredArgsConstructor
public class WebConfiguration implements WebMvcConfigurer {
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");
}
open class WebConfiguration(private val headerInterceptorRest: HeaderInterceptorRest) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(headerInterceptorRest)
}
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 java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.keycloak.adapters.authorization.spi.ConfigurationResolver;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.util.JsonSerialization;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
import de.twomartens.wahlrecht.security.SpringPolicyEnforcerFilter
import org.keycloak.adapters.authorization.spi.ConfigurationResolver
import org.keycloak.adapters.authorization.spi.HttpRequest
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig
import org.keycloak.util.JsonSerialization
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
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
import java.io.IOException
@Configuration
@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(
"/wahlrecht/v1/healthCheck",
"/actuator/**",
"/wahlrecht/v1/doc/**",
"/wahlrecht/v1/api-docs/**",
"/error");
@Value("#{environment.CLIENT_SECRET}")
private lateinit var clientSecret: String
private static final List<PathConfig> PATHS = buildPathConfigs();
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkSetUri;
@Value("#{environment.CLIENT_SECRET}")
private String clientSecret;
@Bean
public SecurityFilterChain securityFilterChain(@NonNull HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers(PERMITTED_PATHS.toArray(new String[0]))
.permitAll()
.and()
.authorizeHttpRequests()
.requestMatchers(HttpMethod.OPTIONS)
.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);
@Bean
@Throws(Exception::class)
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers(*PERMITTED_PATHS.toTypedArray<String>())
.permitAll()
.and()
.authorizeHttpRequests()
.requestMatchers(HttpMethod.OPTIONS)
.permitAll()
.and()
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer { obj: OAuth2ResourceServerConfigurer<HttpSecurity?> -> obj.jwt() }
.addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter::class.java)
return http.build()
}
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.ElectionResultMapper;
import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidates;
import de.twomartens.wahlrecht.model.dto.v1.ElectionResult;
import de.twomartens.wahlrecht.service.CalculationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.twomartens.wahlrecht.mapper.v1.ElectedCandidatesMapper
import de.twomartens.wahlrecht.mapper.v1.ElectionResultMapper
import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidates
import de.twomartens.wahlrecht.model.dto.v1.ElectionResult
import de.twomartens.wahlrecht.service.CalculationService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.mapstruct.factory.Mappers
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/wahlrecht/v1")
@RequestMapping(value = ["/wahlrecht/v1"])
@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(
ElectedCandidatesMapper.class);
private final ElectionResultMapper electionResultMapper = Mappers.getMapper(
ElectionResultMapper.class);
private final CalculationService service;
@Operation(
summary = "Calculates a provided election result",
description = "This request does not store any result and is idempotent.",
responses = {
@ApiResponse(responseCode = "200",
description = "Returns all elected candidates",
content = {@Content(
mediaType = "application/json",
schema = @Schema(implementation = ElectedCandidates.class))}
)
}
)
@PostMapping(value = "/calculate", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
public ResponseEntity<ElectedCandidates> calculateResult(
@RequestBody ElectionResult electionResult
) {
ElectedCandidates result = electedCandidatesMapper.mapToExternal(
service.determineElectedCandidates(electionResultMapper.mapToInternal(electionResult)));
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(result);
}
@Operation(
summary = "Calculates a provided election result",
description = "This request does not store any result and is idempotent.",
responses = [ApiResponse(
responseCode = "200",
description = "Returns all elected candidates",
content = [Content(
mediaType = "application/json",
schema = Schema(implementation = ElectedCandidates::class)
)]
)]
)
@PostMapping(value = ["/calculate"], produces = [MediaType.APPLICATION_JSON_VALUE])
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
fun calculateResult(
@RequestBody electionResult: ElectionResult
): ResponseEntity<ElectedCandidates> {
val 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.model.dto.v1.Election;
import de.twomartens.wahlrecht.service.ElectionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
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;
import de.twomartens.wahlrecht.mapper.v1.ElectionMapper
import de.twomartens.wahlrecht.model.dto.v1.Election
import de.twomartens.wahlrecht.service.ElectionService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.ArraySchema
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/wahlrecht/v1")
@RequestMapping(value = ["/wahlrecht/v1"])
@Tag(name = "Elections", description = "all requests relating to elections")
public class ElectionController {
class ElectionController(private val service: ElectionService) {
private val mapper = Mappers.getMapper(ElectionMapper::class.java)
private final ElectionMapper mapper = Mappers.getMapper(ElectionMapper.class);
private final ElectionService service;
@SecurityRequirement(name = "oauth2")
@SecurityRequirement(name = "bearerAuth")
@GetMapping(value = ["/elections"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation(
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(
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)))}
)
}
)
@GetMapping(value = "/elections", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
public ResponseEntity<Collection<Election>> getElections() {
List<Election> elections = service.getElections().stream().map(mapper::mapToExternal).toList();
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(elections);
}
@Operation(
summary = "Returns election matching provided name",
responses = [ApiResponse(
responseCode = "200",
description = "Returns matching election",
content = [Content(mediaType = "application/json", schema = Schema(implementation = Election::class))]
)]
)
@GetMapping(value = ["/elections/by-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<Election> {
val election = mapper.mapToExternal(service.elections[electionName]!!)
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(election)
}
@Operation(
summary = "Returns election matching provided name",
responses = {
@ApiResponse(responseCode = "200",
description = "Returns matching election",
content = {@Content(
mediaType = "application/json",
schema = @Schema(implementation = Election.class))}
)
}
)
@GetMapping(value = "/elections/by-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE)
@SecurityRequirement(name = "bearerAuth")
@SecurityRequirement(name = "oauth2")
public ResponseEntity<Election> getElectionByName(
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) {
Election election = mapper.mapToExternal(service.getElection(electionName));
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);
}
@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")
fun putElection(@RequestBody election: Election?): ResponseEntity<*> {
val createdNew = service.storeElection(mapper.mapToDB(election!!))
return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(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.model.dto.ErrorMessage;
import de.twomartens.wahlrecht.model.dto.v1.ElectionResult;
import de.twomartens.wahlrecht.service.ElectionResultService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
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;
import de.twomartens.wahlrecht.mapper.v1.ElectionResultMapper
import de.twomartens.wahlrecht.model.dto.ErrorMessage
import de.twomartens.wahlrecht.model.dto.v1.ElectionResult
import de.twomartens.wahlrecht.service.ElectionResultService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import lombok.RequiredArgsConstructor
import lombok.extern.slf4j.Slf4j
import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/wahlrecht/v1")
@RequestMapping(value = ["/wahlrecht/v1"])
@Tag(name = "ElectionResults", description = "all requests relating to election results")
public class ElectionResultController {
class ElectionResultController(private val service: ElectionResultService) {
private val mapper = Mappers.getMapper(ElectionResultMapper::class.java)
private final ElectionResultMapper mapper = Mappers.getMapper(ElectionResultMapper.class);
private final ElectionResultService service;
@Operation(
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(
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")
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);
}
@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")
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)
}
}
@@ -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.model.dto.ErrorMessage;
import de.twomartens.wahlrecht.model.dto.v1.PartyInElection;
import de.twomartens.wahlrecht.service.PartyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
import org.springframework.http.HttpStatus;
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;
import de.twomartens.wahlrecht.mapper.v1.PartyInElectionMapper
import de.twomartens.wahlrecht.model.dto.ErrorMessage
import de.twomartens.wahlrecht.model.dto.v1.PartyInElection
import de.twomartens.wahlrecht.service.PartyService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.ArraySchema
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.mapstruct.factory.Mappers
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.*
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping(value = "/wahlrecht/v1")
@RequestMapping(value = ["/wahlrecht/v1"])
@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);
private final PartyService service;
@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")
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);
}
@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")
fun putParty(@RequestBody party: PartyInElection?): ResponseEntity<*> {
val createdNew = service.storeParty(mapper.mapToDB(party!!))
return if (createdNew) ResponseEntity<Any>(HttpStatus.CREATED) else ResponseEntity<Any>(HttpStatus.OK)
}
}
@@ -256,7 +256,7 @@ class LoggingInterceptorRest(
addLogString(stringMapMessage, PARAM_BUSINESS_TYPE, logMessage.businessType)
addLogString(stringMapMessage, PARAM_REQUEST_BODY, cutToMaxLength(logMessage.requestBody))
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 {
@@ -1,44 +1,24 @@
package de.twomartens.wahlrecht.model.db;
package de.twomartens.wahlrecht.model.db
import java.time.Instant;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
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.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.bson.types.ObjectId
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant
@Document
@Getter
@Setter
@Builder
@EqualsAndHashCode
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Candidate {
data class Candidate(
@Indexed var name: String,
var profession: String
){
@Id
var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude
@Id
ObjectId id;
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
@CreatedDate
lateinit var created: Instant
@Indexed
String name;
String profession;
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -1,46 +1,27 @@
package de.twomartens.wahlrecht.model.db;
package de.twomartens.wahlrecht.model.db
import java.time.Instant;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
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.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
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
import java.time.Instant
@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}")
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
@Id
ObjectId id;
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
@CreatedDate
lateinit var created: Instant
int number;
String electionName;
String name;
int numberOfSeats;
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -1,50 +1,30 @@
package de.twomartens.wahlrecht.model.db;
package de.twomartens.wahlrecht.model.db
import de.twomartens.wahlrecht.model.dto.v1.VotingThreshold;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Collection;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
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.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import de.twomartens.wahlrecht.model.dto.v1.VotingThreshold
import org.bson.types.ObjectId
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant
import java.time.LocalDate
@Document
@Getter
@Setter
@Builder
@EqualsAndHashCode
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Election {
data class Election(
@Indexed(unique = true) var name: String,
var day: LocalDate,
var votingThreshold: VotingThreshold,
var totalNumberOfSeats: Int,
var constituencies: Collection<Constituency>
) {
@Id
var id: ObjectId = ObjectId()
@EqualsAndHashCode.Exclude
@Id
ObjectId id;
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
@CreatedDate
lateinit var created: Instant
@LastModifiedDate
lateinit var lastModified: Instant
@Indexed(unique = true)
String name;
LocalDate day;
VotingThreshold votingThreshold;
int totalNumberOfSeats;
Collection<Constituency> constituencies;
}
@@ -1,44 +1,24 @@
package de.twomartens.wahlrecht.model.db;
package de.twomartens.wahlrecht.model.db
import java.time.Instant;
import java.util.Collection;
import java.util.Map;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
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;
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
import java.time.Instant
@Document
@Getter
@Setter
@Builder
@EqualsAndHashCode
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ElectionResult {
@EqualsAndHashCode.Exclude
@Id
ObjectId id;
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
data class ElectionResult(
var electionName: String,
var overallResults: Collection<VotingResult>,
var constituencyResults: Map<Int, Collection<VotingResult>>
) {
@Id
var id: ObjectId = ObjectId()
String electionName;
Collection<VotingResult> overallResults;
Map<Integer, Collection<VotingResult>> constituencyResults;
@CreatedDate
lateinit var created: Instant
@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 java.util.Map;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
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.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.EqualsAndHashCode
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
import java.time.Instant
@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}")
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
@Id
ObjectId id;
@EqualsAndHashCode.Exclude
@CreatedDate
Instant created;
@EqualsAndHashCode.Exclude
@LastModifiedDate
Instant lastModified;
@EqualsAndHashCode.Exclude
@CreatedDate
lateinit var created: Instant
String abbreviation;
String electionName;
String name;
Nomination overallNomination;
Map<Integer, Nomination> constituencyNominations;
@EqualsAndHashCode.Exclude
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -1,29 +1,10 @@
package de.twomartens.wahlrecht.model.db;
package de.twomartens.wahlrecht.model.db
import java.util.Map;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
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;
}
data class VotingResult(
var electionName: String,
var partyAbbreviation: String,
var nominationName: String,
var votesOnNomination: Int = 0,
var votesThroughHealing: Int = 0,
var votesPerPosition: Map<Int, Int>
)
@@ -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) {
@Override
public String toString() {
return name;
}
data class Constituency(val electionName: String, val number: Int, val name: String, val numberOfSeats: Int) {
override fun toString(): String {
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 {
NOT_ELECTED,
CONSTITUENCY,
OVERALL_NOMINATION_ORDER,
OVERALL_VOTE_ORDER
enum class Elected {
NOT_ELECTED,
CONSTITUENCY,
OVERALL_NOMINATION_ORDER,
OVERALL_VOTE_ORDER
}
@@ -1,13 +1,6 @@
package de.twomartens.wahlrecht.model.internal;
package de.twomartens.wahlrecht.model.internal
public record ElectedCandidate(Candidate candidate, Elected elected) {
public String name() {
return candidate.name();
}
public String profession() {
return candidate.profession();
}
data class ElectedCandidate(val candidate: Candidate, val elected: Elected) {
val name by candidate::name
val profession by candidate::profession
}
@@ -1,9 +1,7 @@
package de.twomartens.wahlrecht.model.internal;
package de.twomartens.wahlrecht.model.internal
import java.util.Collection;
import java.util.Map;
public record ElectionResult(String electionName, Collection<VotingResult> overallResults,
Map<Integer, Collection<VotingResult>> constituencyResults) {
}
data class ElectionResult(
val electionName: String,
val overallResults: Collection<VotingResult>,
val constituencyResults: Map<Int, Collection<VotingResult>>
)
@@ -23,8 +23,8 @@ data class Nomination(val id: NominationId, val supportVotesOnNomination: Boolea
return positionToCandidate.values.toList()
}
fun getCandidate(position: Int): Candidate? {
return positionToCandidate[position]
fun getCandidate(position: Int): Candidate {
return positionToCandidate[position]!!
}
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 java.io.IOException;
import java.time.Clock;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.slf4j.MDC.MDCCloseable;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import mu.KotlinLogging
import org.slf4j.MDC
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.boot.actuate.health.Status
import java.io.IOException
import java.time.Clock
import java.time.Duration
import java.util.*
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractHealthIndicator implements HealthIndicator {
abstract class AbstractHealthIndicator(
private val clock: Clock,
private val preparable: Preparable
) : HealthIndicator {
public static final String HOST = "localhost";
public static final String HTTP_PREFIX = "http://";
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 val logStatusDownMessage = "health indicator '${indicatorName()}' invoked with status '${Status.DOWN.code}'"
private val logStatusUpMessage = "health indicator '${indicatorName()}' invoked with status '${Status.UP.code}'"
private final String logStatusDownMessage = String.format("health indicator '%s' invoked with status '%s'",
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;
private var firstTime = true
/**
* main method that determines the health of the service
*/
protected abstract Health determineHealth();
/**
* main method that determines the health of the service
*/
protected abstract fun determineHealth(): Health
@Override
public Health health() {
try (Closeable ignored = preparable.prepare()) {
Health result = null;
Exception exception = null;
long start = clock.millis();
try {
result = determineHealth();
} catch (RuntimeException e) {
exception = e;
result = Health.down().withException(e).build();
} finally {
logInvocation(result, exception, start, clock.millis());
}
return result;
} catch (IOException e) {
log.error("unexpected exception occurred", e);
return Health.down(e).build();
override fun health(): Health {
try {
preparable.prepare().use {
var result: Health? = null
var exception: Exception? = null
val start = clock.millis()
try {
result = determineHealth()
} catch (e: RuntimeException) {
exception = e
result = Health.down().withException(e).build()
} finally {
logInvocation(result, exception, start, clock.millis())
}
return result!!
}
} catch (e: IOException) {
log.error("unexpected exception occurred", e)
return Health.down(e).build()
}
}
}
private void logInvocation(Health health, Exception exception, long start, long end) {
Duration duration = Duration.ofMillis(end - start);
try (MDCCloseable ignored = MDC.putCloseable("event.duration", Long.toString(duration.toNanos()))) {
if (exception != null || health == null) {
log.error(logStatusDownMessage, exception);
firstTime = true;
} else if (health.getStatus() == Status.DOWN) {
log.warn(logStatusDownMessage);
firstTime = true;
} else if (firstTime) {
log.info(logStatusUpMessage);
firstTime = false;
} else {
log.trace(logStatusUpMessage);
}
private fun logInvocation(health: Health?, exception: Exception?, start: Long, end: Long) {
val duration = Duration.ofMillis(end - start)
MDC.putCloseable("event.duration", duration.toNanos().toString()).use {
if (exception != null || health == null) {
log.error(logStatusDownMessage, exception)
firstTime = true
} else if (health.status === Status.DOWN) {
log.warn(logStatusDownMessage)
firstTime = true
} else if (firstTime) {
log.info(logStatusUpMessage)
firstTime = false
} else {
log.trace(logStatusUpMessage)
}
}
}
}
private String indicatorName() {
return this.getClass().getSimpleName()
.replace("HealthIndicator", "")
.toLowerCase();
}
private fun indicatorName(): String {
return this.javaClass.getSimpleName()
.replace("HealthIndicator", "")
.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 java.time.Clock;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Health.Builder;
import org.springframework.boot.actuate.health.HealthIndicator;
import de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbe
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import java.time.Clock
@Slf4j
public abstract class AbstractStatusProbeHealthIndicator extends AbstractHealthIndicator
implements HealthIndicator {
public static final String MESSAGE_KEY = "message";
public static final String LAST_STATUS_CHANGE_KEY = "lastStatusChange";
private final StatusProbe statusProbe;
public AbstractStatusProbeHealthIndicator(Clock timeProvider, Preparable headerInterceptor,
StatusProbe statusProbe) {
super(timeProvider, headerInterceptor);
this.statusProbe = statusProbe;
}
@Override
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();
}
abstract class AbstractStatusProbeHealthIndicator(
timeProvider: Clock, headerInterceptor: Preparable,
private val statusProbe: StatusProbe
) : AbstractHealthIndicator(timeProvider, headerInterceptor), HealthIndicator {
override fun determineHealth(): Health {
val healthBuilder = Health.status(statusProbe.status)
if (statusProbe.lastStatusChange != null) {
healthBuilder.withDetail(LAST_STATUS_CHANGE_KEY, statusProbe.lastStatusChange)
}
if (statusProbe.throwable != null) {
healthBuilder.withException(statusProbe.throwable)
}
if (statusProbe.message != null) {
healthBuilder.withDetail(MESSAGE_KEY, statusProbe.message)
}
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;
@FunctionalInterface
public interface Preparable {
Closeable prepare();
import java.io.Closeable
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 lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.security.SecureRandom;
import java.time.Clock;
import java.util.Optional;
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.boot.actuate.health.Status
import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.stereotype.Component
import org.springframework.web.client.RestTemplate
import java.security.SecureRandom
import java.time.Clock
/**
* 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
* only tests the
* 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
* balancing. It is possible
* 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.
*/
@Slf4j
@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=";
private final SecureRandom randomizer = new SecureRandom();
private final RestTemplate restTemplateRestHealthIndicator;
private final String urlPrefix;
public RestHealthIndicator(Clock clock, HeaderInterceptorRest interceptor,
ServerProperties serverProperties, RestTemplate restTemplateRestHealthIndicator) {
super(clock, interceptor::markAsHealthCheck);
this.restTemplateRestHealthIndicator = restTemplateRestHealthIndicator;
urlPrefix = HTTP_PREFIX + HOST + HOST_PORT_SEPERATOR + serverProperties.getPort()
+ URL_PATH + PARAMETER_SEPERATOR + GET_PARAMETER;
}
/**
* main method that determines the health of the service
*/
@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();
}
/**
* main method that determines the health of the service
*/
override fun determineHealth(): Health {
val random = randomizer.nextInt(100000, 999999).toString()
val url = "$urlPrefix{random}"
val response = restTemplateRestHealthIndicator.getForEntity(url, String::class.java, random)
val status = if (response.body == random) Status.UP else 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 java.util.concurrent.atomic.AtomicInteger;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.actuate.health.Status
import java.time.Clock
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);
private final int maxFailureCount;
public CountBasedStatusProbe(int maxFailureCount, Clock clock, StatusProbeCriticality criticality, String name,
StatusProbeLogger statusProbeLogger) {
super(clock, criticality, name, statusProbeLogger);
this.maxFailureCount = maxFailureCount;
}
@Override
protected synchronized void setStatus(Status status, Throwable throwable, String message) {
if (status == Status.DOWN) {
int failureCount = this.failureCount.incrementAndGet();
if (failureCount > maxFailureCount) {
super.setStatus(status, throwable, message);
}
} else if (status == Status.UP) {
this.failureCount.set(0);
super.setStatus(status, throwable, message);
@Synchronized
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
if (status === Status.DOWN) {
val failureCount = failureCount.incrementAndGet()
if (failureCount > maxFailureCount) {
super.setStatus(status, throwable, message)
}
} else if (status === Status.UP) {
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 java.time.Duration;
import org.springframework.boot.actuate.health.Status;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.boot.actuate.health.Status
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.time.Clock
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.
* 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.
*/
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;
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;
init {
scheduleTask(threadPoolTaskScheduler, schedulePeriod)
}
requestCount++;
}
private void reset() {
requestCount = 0;
downCount = 0;
throwable = null;
message = null;
}
public synchronized void runScheduledTask() {
if (requestCount > 0 && (downCount * 100.0 / requestCount) > maxFailurePercent) {
super.setStatus(Status.DOWN, throwable, message);
} else {
super.setStatus(Status.UP, null, null);
@Synchronized
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
if (status === Status.DOWN) {
downCount++
this.temporaryThrowable = throwable
this.temporaryMessage = message
}
requestCount++
}
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.support.PeriodicTrigger;
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);
}
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import org.springframework.scheduling.support.PeriodicTrigger
import java.time.Duration
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 java.time.ZonedDateTime;
import lombok.Getter;
import org.springframework.boot.actuate.health.Status;
import lombok.Getter
import org.springframework.boot.actuate.health.Status
import java.time.Clock
import java.time.ZonedDateTime
@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;
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);
init {
statusProbeLogger.registerStatusProbe(name, criticality)
}
this.status = status;
this.throwable = throwable;
this.message = message;
}
public void up() {
setStatus(Status.UP, null, null);
}
protected open fun setStatus(status: Status, throwable: Throwable?, message: String?) {
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) {
setStatus(Status.UP, null, message);
}
fun up() {
setStatus(Status.UP, null, null)
}
public void down() {
setStatus(Status.DOWN, null, null);
}
fun up(message: String) {
setStatus(Status.UP, null, message)
}
public void down(Throwable throwable) {
setStatus(Status.DOWN, throwable, null);
}
fun down() {
setStatus(Status.DOWN, null, null)
}
public void down(String message) {
setStatus(Status.DOWN, null, message);
}
fun down(throwable: Throwable) {
setStatus(Status.DOWN, throwable, null)
}
protected void down(Throwable throwable, String message) {
setStatus(Status.DOWN, throwable, message);
}
fun down(message: String) {
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
public enum StatusProbeCriticality {
K1, K2, K3
enum class StatusProbeCriticality {
K1,
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 static de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality.K2;
import static de.twomartens.wahlrecht.monitoring.statusprobe.StatusProbeCriticality.K3;
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
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;
import java.time.ZonedDateTime;
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;
class StatusProbeLogger internal constructor(private val clock: Clock, private val commLog: Logger) {
private val statusProbeToStatus: MutableMap<ProbeIdent, Status> = mutableMapOf()
public class StatusProbeLogger {
constructor(clock: Clock) : this(clock, LogManager.getLogger("statusprobe"))
private static final Marker MARKER = MarkerManager.getMarker("statusprobe");
private static final String LABEL_CRITICALITY = "label.status.criticality";
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);
fun registerStatusProbe(name: String, criticality: StatusProbeCriticality) {
statusProbeToStatus[ProbeIdent(name, criticality)] = Status.UP
logStatusChange(name, "Startup", Status.UP, ZonedDateTime.now(clock), null)
}
statusProbeToStatus.put(probeIdent, status);
createLog(message, lastStatusChange, throwable);
}
private ProbeIdent getProbeIdent(String name) {
return statusProbeToStatus.keySet().stream().filter(key -> key.name.equals(name)).findFirst().orElse(null);
}
private void createLog(String message, ZonedDateTime lastStatusChange, Throwable throwable) {
Status overallStatus = getOverallStatus();
StatusProbeCriticality criticality = getOverallCriticality();
if (message == null) {
message = "";
fun logStatusChange(
name: String, message: String?, status: Status, lastStatusChange: ZonedDateTime?,
throwable: Throwable?
) {
var probeIdent = getProbeIdent(name)
if (probeIdent == null) {
probeIdent = ProbeIdent(name, StatusProbeCriticality.K1)
}
statusProbeToStatus[probeIdent] = status
createLog(message, lastStatusChange, throwable)
}
if (Status.UP.equals(overallStatus)) {
commLog.info(MARKER, new StringMapMessage()
.with(LABEL_CRITICALITY, criticality)
.with(LABEL_STATUS, overallStatus)
.with(LABEL_MESSAGE, message)
.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 fun getProbeIdent(name: String): ProbeIdent? {
return statusProbeToStatus.keys.asSequence()
.filter { it.name == name }
.firstOrNull()
}
}
private StatusProbeCriticality getOverallCriticality() {
List<StatusProbeCriticality> crits = statusProbeToStatus.keySet().stream().map(key -> key.criticality).toList();
return crits.contains(K1) ? K1 : crits.contains(K2) ? K2 : K3;
}
private Status getOverallStatus() {
if (statusProbeToStatus.containsValue(Status.DOWN)) {
return Status.DOWN;
private fun createLog(message: String?, lastStatusChange: ZonedDateTime?, throwable: Throwable?) {
val cleanedMessage = message ?: ""
val overallStatus = overallStatus
val criticality = overallCriticality
if (Status.UP == overallStatus) {
commLog.info(
MARKER, StringMapMessage()
.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()
.filter(entry -> Status.DOWN.equals(entry.getValue()))
.map(Map.Entry::getKey)
.toList();
private val overallStatus: Status
get() = if (statusProbeToStatus.containsValue(Status.DOWN)) Status.DOWN
else Status.UP
String reasonK1 = getDownStatusProbes(probesDown, K1);
String reasonK2 = getDownStatusProbes(probesDown, K2);
String reasonK3 = getDownStatusProbes(probesDown, K3);
private val reason: String
get() {
val probesDown = statusProbeToStatus.entries.asSequence()
.filter { it.value == Status.DOWN }
.map { it.key }
.toList()
val reasonK1 = getDownStatusProbes(probesDown, StatusProbeCriticality.K1)
val reasonK2 = getDownStatusProbes(probesDown, StatusProbeCriticality.K2)
val reasonK3 = getDownStatusProbes(probesDown, StatusProbeCriticality.K3)
return "$reasonK1$reasonK2$reasonK3".trim { it <= ' ' }
}
return "%s%s%s".formatted(reasonK1, reasonK2, reasonK3).trim();
}
private String getDownStatusProbes(List<ProbeIdent> probesDown, StatusProbeCriticality criticality) {
List<String> downProbeNames = probesDown.stream().filter(probe -> probe.criticality.equals(criticality))
.map(probe -> probe.name).toList();
if (downProbeNames.size() > 0) {
return criticality + " failed: " + String.join(",", downProbeNames) + "\n";
private fun getDownStatusProbes(probesDown: List<ProbeIdent>, criticality: StatusProbeCriticality): String {
val downProbeNames = probesDown.asSequence()
.filter { it.criticality == criticality }
.map { it.name }
.toList()
return if (downProbeNames.isNotEmpty()) {
"$criticality failed: ${downProbeNames.joinToString(separator = ",")}\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 java.time.Duration;
import java.time.ZonedDateTime;
import org.springframework.boot.actuate.health.Status;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.boot.actuate.health.Status
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.time.Clock
import java.time.Duration
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;
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);
init {
scheduleTask(threadPoolTaskScheduler, schedulePeriod)
}
}
private boolean isOverdue() {
if (lastSuccess == null) {
return false;
@Synchronized
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
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() {
if (isOverdue()) {
super.setStatus(Status.DOWN, throwable, message);
private val isOverdue: Boolean
get() {
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 java.util.Optional;
import org.springframework.data.mongodb.repository.MongoRepository;
import de.twomartens.wahlrecht.model.db.Candidate
import org.springframework.data.mongodb.repository.MongoRepository
public interface CandidateRepository extends MongoRepository<Candidate, String> {
Optional<Candidate> findByName(String name);
}
interface CandidateRepository : MongoRepository<Candidate, String>
@@ -1,13 +1,6 @@
package de.twomartens.wahlrecht.repository;
package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.Constituency;
import java.util.Collection;
import java.util.Optional;
import org.springframework.data.mongodb.repository.MongoRepository;
import de.twomartens.wahlrecht.model.db.Constituency
import org.springframework.data.mongodb.repository.MongoRepository
public interface ConstituencyRepository extends MongoRepository<Constituency, String> {
Optional<Constituency> findByElectionNameAndNumber(String electionName, int number);
Collection<Constituency> findByElectionName(String electionName);
}
interface ConstituencyRepository : MongoRepository<Constituency, String>
@@ -1,10 +1,6 @@
package de.twomartens.wahlrecht.repository;
package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.Election;
import java.util.Optional;
import org.springframework.data.mongodb.repository.MongoRepository;
import de.twomartens.wahlrecht.model.db.Election
import org.springframework.data.mongodb.repository.MongoRepository
public interface ElectionRepository extends MongoRepository<Election, String> {
Optional<Election> findByName(String name);
}
interface ElectionRepository : MongoRepository<Election, String>
@@ -1,7 +1,6 @@
package de.twomartens.wahlrecht.repository;
package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.ElectionResult;
import org.springframework.data.mongodb.repository.MongoRepository;
import de.twomartens.wahlrecht.model.db.ElectionResult
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 org.springframework.data.mongodb.repository.MongoRepository;
import de.twomartens.wahlrecht.model.db.Nomination
import org.springframework.data.mongodb.repository.MongoRepository
import java.util.Optional;
public interface NominationRepository extends MongoRepository<Nomination, String> {
Optional<Nomination> findByNameAndElectionNameAndPartyAbbreviation(String name,
String electionName, String partyAbbreviation);
}
interface NominationRepository : MongoRepository<Nomination, String>
@@ -1,14 +1,8 @@
package de.twomartens.wahlrecht.repository;
package de.twomartens.wahlrecht.repository
import de.twomartens.wahlrecht.model.db.PartyInElection;
import java.util.Collection;
import java.util.Optional;
import org.springframework.data.mongodb.repository.MongoRepository;
import de.twomartens.wahlrecht.model.db.PartyInElection
import org.springframework.data.mongodb.repository.MongoRepository
public interface PartyRepository extends MongoRepository<PartyInElection, String> {
Optional<PartyInElection> findByAbbreviationAndElectionName(String abbreviation,
String electionName);
Collection<PartyInElection> findByElectionName(String name);
interface PartyRepository : MongoRepository<PartyInElection, String> {
fun findByElectionName(name: String): Collection<PartyInElection>
}
@@ -12,18 +12,16 @@ import org.keycloak.adapters.authorization.spi.ConfigurationResolver
import org.keycloak.adapters.authorization.spi.HttpRequest
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
import java.io.IOException
import java.util.*
class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver?) : Filter {
private val policyEnforcer: MutableMap<PolicyEnforcerConfig, SpringPolicyEnforcer> = Collections.synchronizedMap(HashMap())
class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver) : Filter {
private val policyEnforcer: MutableMap<PolicyEnforcerConfig, SpringPolicyEnforcer> = mutableMapOf()
@Throws(IOException::class, ServletException::class)
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) {
val request = servletRequest as HttpServletRequest
val response = servletResponse as HttpServletResponse?
val httpRequest = ServletHttpRequest(request
) { extractBearerToken(request) }
val httpRequest = ServletHttpRequest(request) { extractBearerToken(request) }
val policyEnforcer = getOrCreatePolicyEnforcer(httpRequest)
val authzContext = policyEnforcer.enforce(httpRequest, ServletHttpResponse(response))
request.setAttribute(AuthorizationContext::class.java.name, authzContext)
@@ -55,9 +53,7 @@ class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolv
}
private fun getOrCreatePolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer {
return policyEnforcer.computeIfAbsent(configResolver!!.resolve(request)) { enforcerConfig: PolicyEnforcerConfig ->
createPolicyEnforcer(enforcerConfig)
}
return policyEnforcer.computeIfAbsent(configResolver.resolve(request)) { createPolicyEnforcer(it) }
}
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.repository.CandidateRepository;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import de.twomartens.wahlrecht.model.db.Candidate
import de.twomartens.wahlrecht.repository.CandidateRepository
import org.springframework.stereotype.Service
@RequiredArgsConstructor
@Service
public class CandidateService {
private final CandidateRepository repository;
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;
class CandidateService(private val repository: CandidateRepository) {
private val candidates: MutableMap<String, Candidate> by lazy {
fetchCandidates()
}
if (existing != null) {
existing.setProfession(candidate.getProfession());
candidate = existing;
fun storeCandidate(candidate: Candidate): Candidate {
var result = candidate
val existing = candidates[candidate.name]
val needsUpdate = candidate != existing
if (!needsUpdate && existing != null) {
return existing
}
if (existing != null) {
existing.profession = candidate.profession
result = existing
}
result = repository.save(result)
candidates[candidate.name] = candidate
return result
}
candidate = repository.save(candidate);
candidates.put(candidate.getName(), candidate);
return candidate;
}
private void fetchCandidates() {
candidates = repository.findAll().stream()
.collect(Collectors.toMap(Candidate::getName, Function.identity()));
}
private fun fetchCandidates(): MutableMap<String, Candidate> {
return repository.findAll().asSequence()
.map { Pair(it.name, it) }
.toMap()
.toMutableMap()
}
}
@@ -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.internal.ConstituencyId;
import de.twomartens.wahlrecht.repository.ConstituencyRepository;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import de.twomartens.wahlrecht.model.db.Constituency
import de.twomartens.wahlrecht.model.internal.ConstituencyId
import de.twomartens.wahlrecht.repository.ConstituencyRepository
import org.springframework.stereotype.Service
@Service
@RequiredArgsConstructor
public class ConstituencyService {
private final ConstituencyRepository repository;
private Map<ConstituencyId, Constituency> constituencies;
public Constituency storeConstituency(@NonNull Constituency constituency) {
if (constituencies == null) {
fetchConstituencies();
class ConstituencyService(private val repository: ConstituencyRepository) {
private val constituencies: MutableMap<ConstituencyId, Constituency> by lazy {
fetchConstituencies()
}
ConstituencyId constituencyId = new ConstituencyId(constituency.getElectionName(),
constituency.getNumber());
Constituency existing = constituencies.get(constituencyId);
if (constituency.equals(existing)) {
return existing;
fun storeConstituency(constituency: Constituency): Constituency {
var result = constituency
val constituencyId = ConstituencyId(
constituency.electionName,
constituency.number
)
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) {
existing.setNumberOfSeats(constituency.getNumberOfSeats());
existing.setName(constituency.getName());
constituency = existing;
private fun fetchConstituencies(): MutableMap<ConstituencyId, Constituency> {
return repository.findAll().asSequence()
.map { Pair(ConstituencyId(electionName = it.electionName, number = it.number), it) }
.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.repository.ElectionResultRepository;
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;
import de.twomartens.wahlrecht.model.db.ElectionResult
import de.twomartens.wahlrecht.repository.ElectionResultRepository
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
@RequiredArgsConstructor
@Service
public class ElectionResultService {
private final ElectionResultRepository repository;
private Map<String, ElectionResult> electionResults;
public ElectionResult getElectionResult(String electionName) {
if (electionResults == null) {
fetchResults();
class ElectionResultService(private val repository: ElectionResultRepository) {
private val electionResults: MutableMap<String, ElectionResult> by lazy {
fetchResults()
}
ElectionResult electionResult = electionResults.get(electionName);
if (electionResult == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"no election result found for %s".formatted(electionName));
}
return electionResult;
}
public boolean storeResult(ElectionResult result) {
if (electionResults == null) {
fetchResults();
fun getElectionResult(electionName: String): ElectionResult {
return electionResults[electionName]
?: throw ResponseStatusException(
HttpStatus.NOT_FOUND,
"no election result found for $electionName"
)
}
String id = result.getElectionName();
ElectionResult existing = electionResults.get(id);
boolean needsUpdate = !result.equals(existing);
fun storeResult(electionResult: ElectionResult): Boolean {
var result = electionResult
if (!needsUpdate) {
return false;
val id = result.electionName
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;
if (existing != null) {
existing.setOverallResults(result.getOverallResults());
existing.setConstituencyResults(result.getConstituencyResults());
result = existing;
createdNew = false;
private fun fetchResults(): MutableMap<String, ElectionResult> {
return repository.findAll().asSequence()
.map { Pair(it.electionName, it) }
.toMap()
.toMutableMap()
}
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.model.db.Constituency;
import de.twomartens.wahlrecht.model.db.Election;
import de.twomartens.wahlrecht.repository.ElectionRepository;
import java.util.ArrayList;
import java.util.Collection;
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;
import de.twomartens.wahlrecht.mapper.v1.ElectionMapper
import de.twomartens.wahlrecht.model.db.Constituency
import de.twomartens.wahlrecht.model.db.Election
import de.twomartens.wahlrecht.repository.ElectionRepository
import org.mapstruct.factory.Mappers
import org.springframework.stereotype.Service
@RequiredArgsConstructor
@Service
public class ElectionService {
private final ElectionRepository electionRepository;
private final ConstituencyService constituencyService;
private final ElectionMapper electionMapper = Mappers.getMapper(ElectionMapper.class);
private Map<String, Election> elections;
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;
class ElectionService(
private val electionRepository: ElectionRepository,
private val constituencyService: ConstituencyService
) {
private val electionMapper = Mappers.getMapper(ElectionMapper::class.java)
private val _elections: MutableMap<String, Election> by lazy {
fetchElections()
}
Collection<Constituency> constituencies = new ArrayList<>();
election.getConstituencies()
.forEach(constituency -> constituencies.add(
constituencyService.storeConstituency(constituency)));
val elections: Map<String, Election> = _elections
if (existing != null) {
existing.setDay(election.getDay());
existing.setTotalNumberOfSeats(election.getTotalNumberOfSeats());
existing.setVotingThreshold(election.getVotingThreshold());
election = existing;
createdNew = false;
private fun getElection(electionName: String): Election? {
return elections[electionName]
}
election.setConstituencies(constituencies);
Election stored = electionRepository.save(election);
elections.put(electionName, stored);
fun getElectionInternal(electionName: String): de.twomartens.wahlrecht.model.internal.Election {
return electionMapper.mapToInternal(getElection(electionName)!!)
}
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() {
elections = electionRepository.findAll().stream()
.collect(Collectors.toMap(Election::getName, Function.identity()));
}
private fun fetchElections(): MutableMap<String, Election> {
return electionRepository.findAll().asSequence()
.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.PartyInElection;
import de.twomartens.wahlrecht.model.internal.PartyId;
import de.twomartens.wahlrecht.repository.PartyRepository;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
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;
import de.twomartens.wahlrecht.model.db.Nomination
import de.twomartens.wahlrecht.model.db.PartyInElection
import de.twomartens.wahlrecht.model.internal.PartyId
import de.twomartens.wahlrecht.repository.PartyRepository
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
@RequiredArgsConstructor
@Service
public class PartyService {
class PartyService(
private val partyRepository: PartyRepository,
private val nominationService: NominationService
) {
private final PartyRepository partyRepository;
private final NominationService nominationService;
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);
private val parties: MutableMap<PartyId, PartyInElection> by lazy {
fetchParties()
}
return parties;
}
public boolean storeParty(PartyInElection partyInElection) {
if (parties == null) {
fetchParties();
fun getPartiesByElectionName(electionName: String): Collection<PartyInElection> {
val parties = partyRepository.findByElectionName(electionName)
if (parties.isEmpty()) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "no party found for $electionName")
}
return parties
}
boolean createdNew = true;
PartyId partyId = new PartyId(partyInElection.getElectionName(),
partyInElection.getAbbreviation());
PartyInElection existing = parties.get(partyId);
boolean needsUpdate = !partyInElection.equals(existing);
if (!needsUpdate) {
return false;
fun storeParty(partyInElection: PartyInElection): Boolean {
var result = partyInElection
var createdNew = true
val partyId = PartyId(
partyInElection.electionName,
partyInElection.abbreviation
)
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<>();
partyInElection.getConstituencyNominations()
.forEach((key, value) -> constituencyNominations.put(key,
nominationService.storeNomination(value)));
if (existing != null) {
existing.setName(partyInElection.getName());
if (constituencyNominations.isEmpty()) {
constituencyNominations.putAll(existing.getConstituencyNominations());
}
partyInElection = existing;
createdNew = false;
private fun fetchParties(): MutableMap<PartyId, PartyInElection> {
val allParties = partyRepository.findAll()
return allParties.asSequence()
.map { Pair(PartyId(electionName = it.electionName, abbreviation = it.abbreviation), it) }
.toMap()
.toMutableMap()
}
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 java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.logging.log4j.message.StringMapMessage;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import mu.KotlinLogging
import org.apache.logging.log4j.message.StringMapMessage
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.cloud.context.refresh.ContextRefresher
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit
/**
* automatically reloads application*.yaml. reload can also be triggered manually by doing a post
* request on http://localhost:12001/actuator/refresh
*/
@Log4j2
@Service
@RequiredArgsConstructor
public class PropertyReloadService {
public static final int REFRESH_SECONDS = 60;
public static final String PARAM_MESSAGE = "message";
public static final String PARAM_PROPERTIES = "labels.properties";
private final ContextRefresher contextRefresher;
@Scheduled(fixedDelay = REFRESH_SECONDS, initialDelay = REFRESH_SECONDS, timeUnit = TimeUnit.SECONDS)
public void refresh() {
Set<String> properties = contextRefresher.refresh();
if (!properties.isEmpty()) {
log.info(new StringMapMessage()
.with(PARAM_MESSAGE, "properties changed")
.with(PARAM_PROPERTIES, String.join("\n", properties)));
class PropertyReloadService(@Qualifier("configDataContextRefresher") private val contextRefresher: ContextRefresher) {
@Scheduled(
fixedDelay = REFRESH_SECONDS.toLong(),
initialDelay = REFRESH_SECONDS.toLong(),
timeUnit = TimeUnit.SECONDS
)
fun refresh() {
val properties = contextRefresher.refresh()
if (properties.isNotEmpty()) {
log.info(
StringMapMessage()
.with(PARAM_MESSAGE, "properties changed")
.with(PARAM_PROPERTIES, properties.joinToString("\n"))
.asString()
)
}
}
}
companion object {
const val REFRESH_SECONDS = 60
const val PARAM_MESSAGE = "message"
const val PARAM_PROPERTIES = "labels.properties"
private val log = KotlinLogging.logger {}
}
}
@@ -41,6 +41,11 @@ spring:
de.twomartens.wahlrecht:
defaultTimeZone: Europe/Berlin
greeting: "Hello %s"
statusprobe:
scheduleDuration: 60
maxKafkaFailureDuration: 5
maxBlobFailureCount: 5
maxFailurePercent: 50
springdoc:
swagger-ui:
@@ -11,7 +11,6 @@ import org.mockito.Mockito
import org.mockito.junit.jupiter.MockitoExtension
import java.time.LocalDate
import java.time.Month
import java.util.*
import kotlin.collections.set
@ExtendWith(MockitoExtension::class)
@@ -261,12 +260,9 @@ class CalculationServiceTest {
}
private fun buildCandidates(vararg names: String): Collection<Candidate> {
return Arrays.stream(names).map { name: String? ->
Candidate(
name,
""
)
}.toList()
return names
.map { Candidate(it, "") }
.toList()
}
private fun buildVotesPerPosition(vararg votes: Int): Map<Int, Int> {
@@ -339,13 +335,13 @@ class CalculationServiceTest {
@Test
fun shouldCalculateOverallCandidatesCorrectly() {
val seatsPerVotingResult = java.util.Map.of(
SPD_BEZIRK_RESULT, 2,
CDU_BEZIRK_RESULT, 2,
LINKE_BEZIRK_RESULT, 3,
GRUENE_BEZIRK_RESULT, 8,
FDP_BEZIRK_RESULT, 3,
AFD_BEZIRK_RESULT, 3
val seatsPerVotingResult = mapOf(
Pair(SPD_BEZIRK_RESULT, 2),
Pair(CDU_BEZIRK_RESULT, 2),
Pair(LINKE_BEZIRK_RESULT, 3),
Pair(GRUENE_BEZIRK_RESULT, 8),
Pair(FDP_BEZIRK_RESULT, 3),
Pair(AFD_BEZIRK_RESULT, 3)
)
val electedCandidates = setUpConstituencyResults()
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(candidates)
.anyMatch { candidate ->
(candidate.name().equals(GOTTLIEB_GABOR)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == GOTTLIEB_GABOR
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(RIEGEL_ANN_KATHRIN)
&& candidate.elected === Elected.OVERALL_VOTE_ORDER)
(candidate.name == RIEGEL_ANN_KATHRIN
&& candidate.elected == Elected.OVERALL_VOTE_ORDER)
}
}
.anySatisfy { id, candidates ->
@@ -377,14 +373,13 @@ class CalculationServiceTest {
.isEqualTo(CDU_BEZIRK.id)
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(HOEFLICH_JUTTA)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == HOEFLICH_JUTTA
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name()
.equals(DR_LANGHEIN_A_W_HEINRICH)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
(candidate.name == DR_LANGHEIN_A_W_HEINRICH
&& candidate.elected == Elected.OVERALL_VOTE_ORDER)
}
}
.anySatisfy { id, candidates ->
@@ -392,18 +387,18 @@ class CalculationServiceTest {
.isEqualTo(LINKE_BEZIRK.id)
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(PAGELS_MANUELA)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == PAGELS_MANUELA
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(WIEGMANN_ROLAND)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == WIEGMANN_ROLAND
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(KLEINERT_MIKEY)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
(candidate.name == KLEINERT_MIKEY
&& candidate.elected == Elected.OVERALL_VOTE_ORDER)
}
}
.anySatisfy { id, candidates ->
@@ -411,19 +406,18 @@ class CalculationServiceTest {
.isEqualTo(FDP_BEZIRK.id)
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(KRUEGER_KLAUS)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == KRUEGER_KLAUS
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(SCHWANKE_BENJAMIN)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == SCHWANKE_BENJAMIN
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name()
.equals(MUELLER_SOENKSEN_BURKHARDT)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
(candidate.name == MUELLER_SOENKSEN_BURKHARDT
&& candidate.elected == Elected.OVERALL_VOTE_ORDER)
}
}
.anySatisfy { id, candidates ->
@@ -431,18 +425,18 @@ class CalculationServiceTest {
.isEqualTo(AFD_BEZIRK.id)
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(SCHOEMER_DIRK)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == SCHOEMER_DIRK
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(ZIMMERMANN_ELKE)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == ZIMMERMANN_ELKE
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(PILLATZKE_JOERG)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
(candidate.name == PILLATZKE_JOERG
&& candidate.elected == Elected.OVERALL_VOTE_ORDER)
}
}
.anySatisfy { id, candidates ->
@@ -450,62 +444,63 @@ class CalculationServiceTest {
.isEqualTo(GRUENE_BEZIRK.id)
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(KUHLMANN_DIETMAR)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == KUHLMANN_DIETMAR
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(KLEIN_ROBERT)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == KLEIN_ROBERT
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(ERK_ARAMAK)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == ERK_ARAMAK
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(MARTENS_JIM)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == MARTENS_JIM
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name()
.equals(DR_FISCHER_JOST_LEONHARDT)
&& candidate.elected() === Elected.OVERALL_NOMINATION_ORDER)
(candidate.name == DR_FISCHER_JOST_LEONHARDT
&& candidate.elected == Elected.OVERALL_NOMINATION_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(KUELL_GABRIELA)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
(candidate.name == KUELL_GABRIELA
&& candidate.elected == Elected.OVERALL_VOTE_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(BOHNY_CARL_MARIA)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
(candidate.name == BOHNY_CARL_MARIA
&& candidate.elected == Elected.OVERALL_VOTE_ORDER)
}
Assertions.assertThat(candidates)
.anyMatch { candidate ->
(candidate.name().equals(DORSCH_SEBASTIAN)
&& candidate.elected() === Elected.OVERALL_VOTE_ORDER)
(candidate.name == DORSCH_SEBASTIAN
&& candidate.elected == Elected.OVERALL_VOTE_ORDER)
}
}
}
private fun setUpConstituencyResults(): Map<String, Collection<ElectedCandidate>> {
return java.util.Map.of(
GRUENE_BEZIRK_RESULT.nominationId.partyAbbreviation, listOf(
ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(3), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(5), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(6), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(7), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(11), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(15), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(16), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(20), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(22), Elected.CONSTITUENCY)
return mapOf(
Pair(
GRUENE_BEZIRK_RESULT.nominationId.partyAbbreviation, listOf(
ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(3), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(5), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(6), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(7), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(11), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(15), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(16), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(20), Elected.CONSTITUENCY),
ElectedCandidate(GRUENE_BEZIRK.getCandidate(22), Elected.CONSTITUENCY)
)
)
)
}