Moved plenty files over to Kotlin
This commit is contained in:
@@ -1,13 +1,7 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="MainApplication" type="Application" factoryName="Application" nameIsGenerated="true">
|
<configuration default="false" name="MainApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
<option name="MAIN_CLASS_NAME" value="de.twomartens.wahlrecht.MainApplication" />
|
|
||||||
<module name="wahlrecht.server.main" />
|
<module name="wahlrecht.server.main" />
|
||||||
<extension name="coverage">
|
<option name="SPRING_BOOT_MAIN_CLASS" value="de.twomartens.wahlrecht.MainApplication" />
|
||||||
<pattern>
|
|
||||||
<option name="PATTERN" value="de.twomartens.wahlrecht.*" />
|
|
||||||
<option name="ENABLED" value="true" />
|
|
||||||
</pattern>
|
|
||||||
</extension>
|
|
||||||
<extension name="net.ashald.envfile">
|
<extension name="net.ashald.envfile">
|
||||||
<option name="IS_ENABLED" value="true" />
|
<option name="IS_ENABLED" value="true" />
|
||||||
<option name="IS_SUBST" value="false" />
|
<option name="IS_SUBST" value="false" />
|
||||||
|
|||||||
@@ -8,4 +8,11 @@ val libs = the<LibrariesForLibs>()
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.kotlin.logging)
|
implementation(libs.kotlin.logging)
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-Xjvm-default=all")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,9 @@ junit = "5.9.2"
|
|||||||
assertj = "3.24.2"
|
assertj = "3.24.2"
|
||||||
mockito = "5.3.0"
|
mockito = "5.3.0"
|
||||||
keycloak = "22.0.0"
|
keycloak = "22.0.0"
|
||||||
kotlin-logging = "2.0.11"
|
kotlin-logging = "3.0.5"
|
||||||
|
kotlin-reflect = "1.9.0"
|
||||||
|
kotlin-lombok = "1.9.0"
|
||||||
plugin-nebula-release = "17.2.2"
|
plugin-nebula-release = "17.2.2"
|
||||||
plugin-lombok = "8.0.1"
|
plugin-lombok = "8.0.1"
|
||||||
plugin-gradle-versions = "0.46.0"
|
plugin-gradle-versions = "0.46.0"
|
||||||
@@ -70,6 +72,7 @@ mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
|
|||||||
mockito-inline = "org.mockito:mockito-inline:5.2.0"
|
mockito-inline = "org.mockito:mockito-inline:5.2.0"
|
||||||
mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
|
mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
|
||||||
kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlin-logging" }
|
kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlin-logging" }
|
||||||
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||||
plugin-nebula-release = { module = "com.netflix.nebula:nebula-release-plugin", version.ref = "plugin-nebula-release" }
|
plugin-nebula-release = { module = "com.netflix.nebula:nebula-release-plugin", version.ref = "plugin-nebula-release" }
|
||||||
plugin-springboot = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "spring-boot" }
|
plugin-springboot = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "spring-boot" }
|
||||||
plugin-lombok = { module = "io.freefair.gradle:lombok-plugin", version.ref = "plugin-lombok" }
|
plugin-lombok = { module = "io.freefair.gradle:lombok-plugin", version.ref = "plugin-lombok" }
|
||||||
@@ -128,3 +131,6 @@ test = [
|
|||||||
"mockito-inline",
|
"mockito-inline",
|
||||||
"mockito-junit",
|
"mockito-junit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
kotlin-lombok = { id = "org.jetbrains.kotlin.plugin.lombok", version.ref = "kotlin-lombok" }
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("twomartens.spring-boot-cloud")
|
id("twomartens.spring-boot-cloud")
|
||||||
id("twomartens.kotlin")
|
id("twomartens.kotlin")
|
||||||
|
kotlin("kapt")
|
||||||
|
alias(libs.plugins.kotlin.lombok)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.mapstruct.base)
|
implementation(libs.mapstruct.base)
|
||||||
implementation(libs.bundles.spring.boot.security)
|
implementation(libs.bundles.spring.boot.security)
|
||||||
annotationProcessor(libs.mapstruct.processor)
|
annotationProcessor(libs.mapstruct.processor)
|
||||||
|
kapt(libs.mapstruct.processor)
|
||||||
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
keepJavacAnnotationProcessors = true
|
||||||
}
|
}
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.configuration;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.property.ServiceProperties;
|
|
||||||
import java.time.Clock;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ClockConfiguration {
|
|
||||||
|
|
||||||
private final ServiceProperties serviceProperties;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Clock clock() {
|
|
||||||
return Clock.system(serviceProperties.getDefaultTimeZone());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.configuration;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest;
|
|
||||||
import de.twomartens.wahlrecht.interceptor.LoggingInterceptorRest;
|
|
||||||
import java.time.Clock;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class InterceptorConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public LoggingInterceptorRest loggingInterceptorRest(Clock clock) {
|
|
||||||
return new LoggingInterceptorRest(clock);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public HeaderInterceptorRest headerInterceptorRest() {
|
|
||||||
return new HeaderInterceptorRest();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.configuration;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest;
|
|
||||||
import de.twomartens.wahlrecht.interceptor.LoggingInterceptorRest;
|
|
||||||
import de.twomartens.wahlrecht.property.RestTemplateTimeoutProperties;
|
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class RestTemplateConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public RestTemplate restTemplate(HeaderInterceptorRest headerInterceptorRest,
|
|
||||||
LoggingInterceptorRest loggingInterceptor,
|
|
||||||
RestTemplateTimeoutProperties restTemplateTimeoutProperties) {
|
|
||||||
return new RestTemplateBuilder()
|
|
||||||
.additionalInterceptors(headerInterceptorRest, loggingInterceptor)
|
|
||||||
.setConnectTimeout(restTemplateTimeoutProperties.getConnectionRestTemplateTimeoutInMillis())
|
|
||||||
.setReadTimeout(restTemplateTimeoutProperties.getReadTimeoutRestTemplateInMillis())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public RestTemplate restTemplateRestHealthIndicator(HeaderInterceptorRest headerInterceptorRest,
|
|
||||||
RestTemplateTimeoutProperties restTemplateTimeoutProperties) {
|
|
||||||
return new RestTemplateBuilder()
|
|
||||||
.additionalInterceptors(headerInterceptorRest)
|
|
||||||
.setConnectTimeout(restTemplateTimeoutProperties.getConnectionRestHealthIndicatorTimeoutInMillis())
|
|
||||||
.setReadTimeout(restTemplateTimeoutProperties.getReadTimeoutRestHealthIndicatorInMillis())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.controller;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.exception.HttpStatusException;
|
|
||||||
import de.twomartens.wahlrecht.model.dto.ErrorMessage;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
|
||||||
|
|
||||||
@ControllerAdvice(annotations = RestController.class)
|
|
||||||
@Slf4j
|
|
||||||
public class ExceptionController extends ResponseEntityExceptionHandler {
|
|
||||||
|
|
||||||
@ExceptionHandler(HttpStatusException.class)
|
|
||||||
public ResponseEntity<ErrorMessage> handleException(HttpStatusException e) {
|
|
||||||
if (e.getCause() != null) {
|
|
||||||
log.info(e.getCause().toString(), e.getCause());
|
|
||||||
} else {
|
|
||||||
log.info(e.toString());
|
|
||||||
}
|
|
||||||
return ResponseEntity.status(e.getStatus()).body(ErrorMessage.builder()
|
|
||||||
.message(e.getMessage())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(RuntimeException.class)
|
|
||||||
public ResponseEntity<ErrorMessage> handleRuntimeException(RuntimeException e) {
|
|
||||||
log.error("unexpected exception occurred", e);
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorMessage.builder()
|
|
||||||
.message(e.getMessage())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-89
@@ -1,89 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.controller;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.jar.JarFile;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* used to show version and title information on html pages
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Controller
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@RequestMapping(value = "/wahlrecht")
|
|
||||||
public class VersionHtmlController {
|
|
||||||
|
|
||||||
@GetMapping(path = "/version")
|
|
||||||
public String version() {
|
|
||||||
return "version";
|
|
||||||
}
|
|
||||||
|
|
||||||
@ModelAttribute("version")
|
|
||||||
private String getApplicationVersion() {
|
|
||||||
return getTitle() + " " + getVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
@ModelAttribute("footerString")
|
|
||||||
private String getApplicationVersion(@RequestHeader("host") String hostName) {
|
|
||||||
return getTitle() + " " + getVersion() + " - " + hostName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTitle() {
|
|
||||||
return Optional.ofNullable(VersionHtmlController.class.getPackage().getImplementationTitle())
|
|
||||||
.filter(s -> !s.isBlank())
|
|
||||||
.orElse("application");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getVersion() {
|
|
||||||
return Optional.ofNullable(VersionHtmlController.class.getPackage().getImplementationVersion())
|
|
||||||
.filter(s -> !s.isBlank())
|
|
||||||
.orElse("DEVELOPER");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ModelAttribute("hostname")
|
|
||||||
private String getHostname() {
|
|
||||||
try {
|
|
||||||
return InetAddress.getLocalHost().getHostName();
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
log.warn(e.toString(), e);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
@ModelAttribute("manifest")
|
|
||||||
private Collection<String> getManifest() {
|
|
||||||
try {
|
|
||||||
URL location = getClass().getProtectionDomain().getCodeSource().getLocation();
|
|
||||||
String jarFileName = Paths.get(location.toURI()).toString();
|
|
||||||
try (JarFile jarFile = new JarFile(jarFileName)) {
|
|
||||||
ZipEntry entry = jarFile.getEntry(JarFile.MANIFEST_NAME);
|
|
||||||
try (InputStream in = jarFile.getInputStream(entry)) {
|
|
||||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines().toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (FileNotFoundException ignored) {
|
|
||||||
// do nothing if manifest file is not available
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.info(e.toString(), e);
|
|
||||||
}
|
|
||||||
return List.of(getTitle() + " " + getVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.exception;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class HttpStatusException extends RuntimeException {
|
|
||||||
|
|
||||||
private final HttpStatus status;
|
|
||||||
|
|
||||||
public HttpStatusException(HttpStatus status, String message) {
|
|
||||||
super(message);
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpStatusException(HttpStatus status, String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-82
@@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2002-2017 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.twomartens.wahlrecht.interceptor;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatusCode;
|
|
||||||
import org.springframework.http.client.ClientHttpResponse;
|
|
||||||
import org.springframework.lang.Nullable;
|
|
||||||
import org.springframework.util.StreamUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple implementation of {@link ClientHttpResponse} that reads the response's body into memory, thus allowing for
|
|
||||||
* multiple invocations of {@link #getBody()}.
|
|
||||||
*
|
|
||||||
* @author Arjen Poutsma
|
|
||||||
* @since 3.1
|
|
||||||
*/
|
|
||||||
public final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {
|
|
||||||
|
|
||||||
private final ClientHttpResponse response;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private byte[] body;
|
|
||||||
|
|
||||||
|
|
||||||
public BufferingClientHttpResponseWrapper(ClientHttpResponse response) {
|
|
||||||
this.response = response;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HttpStatusCode getStatusCode() throws IOException {
|
|
||||||
return this.response.getStatusCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Deprecated
|
|
||||||
public int getRawStatusCode() throws IOException {
|
|
||||||
return this.response.getRawStatusCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getStatusText() throws IOException {
|
|
||||||
return this.response.getStatusText();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HttpHeaders getHeaders() {
|
|
||||||
return this.response.getHeaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputStream getBody() throws IOException {
|
|
||||||
if (this.body == null) {
|
|
||||||
this.body = StreamUtils.copyToByteArray(this.response.getBody());
|
|
||||||
}
|
|
||||||
return new ByteArrayInputStream(this.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
this.response.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-87
@@ -1,87 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.interceptor;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.slf4j.MDC;
|
|
||||||
|
|
||||||
public abstract class HeaderInterceptor {
|
|
||||||
|
|
||||||
public static final String LOGGER_TRACE_ID = "trace.id";
|
|
||||||
public static final String LOGGER_REQTYPE_ID = "REQTYPE";
|
|
||||||
|
|
||||||
public static final String HEADER_FIELD_TRACE_ID = "X-TraceId";
|
|
||||||
public static final String HEADER_FIELD_B3_TRACE_ID = "x-b3-traceid";
|
|
||||||
public static final String HEADER_FIELD_TYPE_ID = "x-type";
|
|
||||||
|
|
||||||
public static final String REQ_TYPE_HEALTHCHECK = "HEALTH_CHECK";
|
|
||||||
public static final String REQ_TYPE_INTEGRATION_TEST = "INTEGRATION_TEST";
|
|
||||||
public static final String REQ_TYPE_SERVER_TEST = "SERVER_TEST";
|
|
||||||
public static final String REQ_TYPE_WARMUP = "WARMUP";
|
|
||||||
|
|
||||||
public String getTraceId() {
|
|
||||||
return Optional.ofNullable(MDC.get(LOGGER_TRACE_ID))
|
|
||||||
.filter(s -> !s.isBlank())
|
|
||||||
.orElse(createNewTraceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> getRequestType() {
|
|
||||||
return Optional.ofNullable(MDC.get(LOGGER_REQTYPE_ID))
|
|
||||||
.filter(requestType -> !requestType.isBlank());
|
|
||||||
}
|
|
||||||
|
|
||||||
public InterceptorCloseables setTraceId(String traceId) {
|
|
||||||
return new InterceptorCloseables(MDC.putCloseable(LOGGER_TRACE_ID, traceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public InterceptorCloseables mark(String requestType) {
|
|
||||||
return new InterceptorCloseables(MDC.putCloseable(LOGGER_REQTYPE_ID, requestType));
|
|
||||||
}
|
|
||||||
|
|
||||||
public InterceptorCloseables set(String traceId, String requestType) {
|
|
||||||
return requestType != null
|
|
||||||
? new InterceptorCloseables(setTraceId(traceId), mark(requestType))
|
|
||||||
: setTraceId(traceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InterceptorCloseables markAsHealthCheck() {
|
|
||||||
return new InterceptorCloseables(mark(REQ_TYPE_HEALTHCHECK), setTraceId(createNewTraceId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public InterceptorCloseables markAsIntegrationTest() {
|
|
||||||
return new InterceptorCloseables(mark(REQ_TYPE_INTEGRATION_TEST), setTraceId(createNewTraceId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public InterceptorCloseables markAsServerTest() {
|
|
||||||
return new InterceptorCloseables(mark(REQ_TYPE_SERVER_TEST), setTraceId(createNewTraceId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public InterceptorCloseables markAsWarmup() {
|
|
||||||
return new InterceptorCloseables(mark(REQ_TYPE_WARMUP), setTraceId(createNewTraceId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String createNewTraceId() {
|
|
||||||
return UUID.randomUUID().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class InterceptorCloseables implements Closeable {
|
|
||||||
|
|
||||||
private final Closeable[] closeables;
|
|
||||||
|
|
||||||
private InterceptorCloseables(Closeable... closeables) {
|
|
||||||
this.closeables = closeables;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
for (Closeable closeable : closeables) {
|
|
||||||
try {
|
|
||||||
closeable.close();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-68
@@ -1,68 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.interceptor;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.springframework.http.HttpRequest;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
|
||||||
import org.springframework.http.client.ClientHttpResponse;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
|
||||||
|
|
||||||
public class HeaderInterceptorRest extends HeaderInterceptor
|
|
||||||
implements HandlerInterceptor, ClientHttpRequestInterceptor {
|
|
||||||
|
|
||||||
public static final String CLASS_NAME = HeaderInterceptorRest.class.getName();
|
|
||||||
|
|
||||||
// ClientHttpRequestInterceptor
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public ClientHttpResponse intercept(HttpRequest request, @NonNull byte[] body,
|
|
||||||
ClientHttpRequestExecution execution) throws IOException {
|
|
||||||
request.getHeaders().add(HEADER_FIELD_TRACE_ID, getTraceId());
|
|
||||||
getRequestType().ifPresent(requestType -> request.getHeaders().add(HEADER_FIELD_TYPE_ID, requestType));
|
|
||||||
try {
|
|
||||||
return execution.execute(request, body);
|
|
||||||
} finally {
|
|
||||||
request.getHeaders().remove(HEADER_FIELD_TRACE_ID);
|
|
||||||
request.getHeaders().remove(HEADER_FIELD_TYPE_ID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandlerInterceptor
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
|
|
||||||
@NonNull Object handler) {
|
|
||||||
String traceId = extractTraceId(request);
|
|
||||||
String requestType = extractRequestType(request);
|
|
||||||
InterceptorCloseables closeable = set(traceId, requestType);
|
|
||||||
request.setAttribute(CLASS_NAME, closeable);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String extractTraceId(HttpServletRequest request) {
|
|
||||||
return Optional.ofNullable(request.getHeader(HEADER_FIELD_TRACE_ID)).filter(s -> !s.isBlank())
|
|
||||||
.or(() -> Optional.ofNullable(request.getHeader(HEADER_FIELD_B3_TRACE_ID)).filter(s -> !s.isBlank()))
|
|
||||||
.orElse(UUID.randomUUID().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String extractRequestType(HttpServletRequest request) {
|
|
||||||
return Optional.ofNullable(request.getHeader(HEADER_FIELD_TYPE_ID))
|
|
||||||
.filter(reqType -> !reqType.isBlank())
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandlerInterceptor
|
|
||||||
@Override
|
|
||||||
public void postHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
|
|
||||||
@NonNull Object handler, ModelAndView modelAndView) {
|
|
||||||
Optional.ofNullable(request.getAttribute(CLASS_NAME))
|
|
||||||
.filter(InterceptorCloseables.class::isInstance)
|
|
||||||
.map(InterceptorCloseables.class::cast)
|
|
||||||
.ifPresent(InterceptorCloseables::close);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-419
@@ -1,419 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.interceptor;
|
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.time.Clock;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
import org.apache.logging.log4j.Marker;
|
|
||||||
import org.apache.logging.log4j.MarkerManager.Log4jMarker;
|
|
||||||
import org.apache.logging.log4j.message.StringMapMessage;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpRequest;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
|
||||||
import org.springframework.http.client.ClientHttpResponse;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.util.StreamUtils;
|
|
||||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
|
||||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
|
||||||
|
|
||||||
@Log4j2
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class LoggingInterceptorRest implements Filter, ClientHttpRequestInterceptor {
|
|
||||||
|
|
||||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
|
||||||
public static final Marker MARKER = new Log4jMarker("communication");
|
|
||||||
|
|
||||||
private static final int MAX_LOG_SIZE = 20480; // 20 KB - logging could fail with bigger logmessages
|
|
||||||
|
|
||||||
public static final String DIRECTION_IN = "inbound";
|
|
||||||
public static final String DIRECTION_OUT = "outbound";
|
|
||||||
public static final String PROTOCOL_NAME = "http";
|
|
||||||
|
|
||||||
public static final String PARAM_URL_FULL = "url.full";
|
|
||||||
public static final String PARAM_URL_DOMAIN = "url.domain";
|
|
||||||
public static final String PARAM_URL_EXTENSION = "url.extension";
|
|
||||||
public static final String PARAM_URL_PATH = "url.path";
|
|
||||||
public static final String PARAM_URL_PORT = "url.port";
|
|
||||||
public static final String PARAM_URL_SCHEME = "url.scheme";
|
|
||||||
public static final String PARAM_URL_QUERY = "url.query";
|
|
||||||
public static final String PARAM_BUSINESS_TYPE = "http.request.type";
|
|
||||||
public static final String PARAM_REQUEST_BODY = "http.request.body.content";
|
|
||||||
public static final String PARAM_RESPONSE_BODY = "http.response.body.content";
|
|
||||||
public static final String PARAM_RESPONSE_STATUS = "http.response.status_code";
|
|
||||||
public static final String PARAM_REQUEST_HEADERS = "http.request.headers";
|
|
||||||
public static final String PARAM_RESPONSE_HEADERS = "http.response.headers";
|
|
||||||
public static final String PARAM_REQUEST_BYTES = "http.request.body.bytes";
|
|
||||||
public static final String PARAM_RESPONSE_BYTES = "http.response.body.bytes";
|
|
||||||
public static final String PARAM_REQUEST_MIMETYPE = "http.request.mime_type";
|
|
||||||
public static final String PARAM_RESPONSE_MIMETYPE = "http.response.mime_type";
|
|
||||||
public static final String PARAM_REQUEST_METHOD = "http.request.method";
|
|
||||||
public static final String PARAM_REQUEST_REFERER = "http.request.referrer";
|
|
||||||
public static final String PARAM_REQUEST_TIME = "event.start";
|
|
||||||
public static final String PARAM_RESPONSE_TIME = "event.end";
|
|
||||||
public static final String PARAM_DURATION = "event.duration";
|
|
||||||
public static final String PARAM_USER_AGENT = "user_agent.original";
|
|
||||||
public static final String PARAM_DIRECTION = "network.direction";
|
|
||||||
public static final String PARAM_PROTOCOL = "network.protocol";
|
|
||||||
|
|
||||||
private final FieldLogBehaviour requestLogBehaviour;
|
|
||||||
private final FieldLogBehaviour responseLogBehaviour;
|
|
||||||
private final Clock clock;
|
|
||||||
|
|
||||||
public LoggingInterceptorRest(Clock clock) {
|
|
||||||
this(FieldLogBehaviour.NEVER, FieldLogBehaviour.NEVER, clock);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse response,
|
|
||||||
FilterChain chain) throws IOException, ServletException {
|
|
||||||
ZonedDateTime requestTime = ZonedDateTime.now(clock);
|
|
||||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
|
||||||
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpRequest);
|
|
||||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(
|
|
||||||
(HttpServletResponse) response);
|
|
||||||
|
|
||||||
byte[] fullResponseBytes = null;
|
|
||||||
Throwable throwable = null;
|
|
||||||
String responseBody = null;
|
|
||||||
int httpStatusCode = -1;
|
|
||||||
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
chain.doFilter(
|
|
||||||
requestLogBehaviour != FieldLogBehaviour.NEVER ? requestWrapper : httpRequest,
|
|
||||||
responseWrapper);
|
|
||||||
if (responseLogBehaviour != FieldLogBehaviour.NEVER) {
|
|
||||||
fullResponseBytes = responseWrapper.getContentAsByteArray();
|
|
||||||
}
|
|
||||||
httpStatusCode = responseWrapper.getStatus();
|
|
||||||
} finally {
|
|
||||||
responseWrapper.copyBodyToResponse();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throwable = e;
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
int responseSize = responseWrapper.getContentSize();
|
|
||||||
Map<String, Collection<String>> responseHeaders = extractHeaders(
|
|
||||||
responseWrapper.getHeaderNames().iterator(),
|
|
||||||
s -> responseWrapper.getHeaders(s).iterator());
|
|
||||||
if ((responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
|
||||||
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS) && fullResponseBytes != null) {
|
|
||||||
responseBody = new String(fullResponseBytes,
|
|
||||||
determineResponseEncoding(responseHeaders, fullResponseBytes));
|
|
||||||
}
|
|
||||||
URL requestUrl = new URL(Optional.ofNullable(httpRequest.getQueryString())
|
|
||||||
.map(qs -> httpRequest.getRequestURL().toString() + "?" + qs)
|
|
||||||
.orElse(httpRequest.getRequestURL().toString()));
|
|
||||||
Map<String, Collection<String>> requestHeaders = extractHeaders(
|
|
||||||
httpRequest.getHeaderNames().asIterator(),
|
|
||||||
s -> httpRequest.getHeaders(s).asIterator());
|
|
||||||
String requestBody = null;
|
|
||||||
String businessType = null;
|
|
||||||
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
|
||||||
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS) {
|
|
||||||
byte[] fullRequestBytes = requestWrapper.getContentAsByteArray();
|
|
||||||
if (fullRequestBytes != null) {
|
|
||||||
requestBody = new String(fullRequestBytes,
|
|
||||||
determineRequestEncoding(requestHeaders, fullRequestBytes));
|
|
||||||
}
|
|
||||||
businessType = determineBusinessType(requestUrl, requestBody);
|
|
||||||
}
|
|
||||||
log(LogMessage.builder()
|
|
||||||
.requestHeaders(requestHeaders)
|
|
||||||
.responseHeaders(responseHeaders)
|
|
||||||
.url(requestUrl)
|
|
||||||
.method(httpRequest.getMethod())
|
|
||||||
.requestMimeType(typeToString(request.getContentType()))
|
|
||||||
.responseMimeType(typeToString(response.getContentType()))
|
|
||||||
.requestBody(requestBody)
|
|
||||||
.responseBody(responseBody)
|
|
||||||
.requestSize(httpRequest.getContentLength())
|
|
||||||
.responseSize(responseSize)
|
|
||||||
.httpStatus(httpStatusCode)
|
|
||||||
.direction(DIRECTION_IN)
|
|
||||||
.requestTime(requestTime)
|
|
||||||
.responseTime(ZonedDateTime.now(clock))
|
|
||||||
.traceId(HeaderInterceptorRest.extractTraceId(httpRequest))
|
|
||||||
.requestType(HeaderInterceptorRest.extractRequestType(httpRequest))
|
|
||||||
.businessType(businessType)
|
|
||||||
.throwable(throwable)
|
|
||||||
.build());
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
log.error(e.toString(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isError(int httpStatusCode) {
|
|
||||||
return httpStatusCode >= 400 && httpStatusCode < 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ClientHttpResponse intercept(@NonNull HttpRequest request,
|
|
||||||
@NonNull byte[] requestBytes, @NonNull ClientHttpRequestExecution execution)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
ZonedDateTime requestTime = ZonedDateTime.now(clock);
|
|
||||||
int responseSize = 0;
|
|
||||||
Map<String, Collection<String>> responseHeaders = Collections.emptyMap();
|
|
||||||
MediaType responseMediaType = null;
|
|
||||||
int httpStatusCode = -1;
|
|
||||||
Throwable throwable = null;
|
|
||||||
String requestBody = null;
|
|
||||||
String responseBody = null;
|
|
||||||
String businessType = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
BufferingClientHttpResponseWrapper result = new BufferingClientHttpResponseWrapper(
|
|
||||||
execution.execute(request, requestBytes));
|
|
||||||
byte[] responseBytes = StreamUtils.copyToByteArray(result.getBody());
|
|
||||||
responseSize = responseBytes.length;
|
|
||||||
responseHeaders = extractHeaders(result.getHeaders());
|
|
||||||
responseMediaType = result.getHeaders().getContentType();
|
|
||||||
httpStatusCode = result.getStatusCode().value();
|
|
||||||
if (responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
|
||||||
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS) {
|
|
||||||
responseBody = new String(responseBytes,
|
|
||||||
determineRequestEncoding(responseHeaders, responseBytes));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (Exception e) {
|
|
||||||
throwable = e;
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
URL url = request.getURI().toURL();
|
|
||||||
Map<String, Collection<String>> requestHeaders = extractHeaders(request.getHeaders());
|
|
||||||
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
|
||||||
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS) {
|
|
||||||
requestBody = new String(requestBytes,
|
|
||||||
determineRequestEncoding(requestHeaders, requestBytes));
|
|
||||||
businessType = determineBusinessType(url, requestBody);
|
|
||||||
}
|
|
||||||
log(LogMessage.builder()
|
|
||||||
.requestHeaders(requestHeaders)
|
|
||||||
.responseHeaders(responseHeaders)
|
|
||||||
.url(url)
|
|
||||||
.method(Objects.requireNonNull(request.getMethod()).toString())
|
|
||||||
.requestMimeType(typeToString(request.getHeaders().getContentType()))
|
|
||||||
.requestBody(requestBody)
|
|
||||||
.responseBody(responseBody)
|
|
||||||
.responseMimeType(typeToString(responseMediaType))
|
|
||||||
.requestSize(requestBytes.length)
|
|
||||||
.responseSize(responseSize)
|
|
||||||
.httpStatus(httpStatusCode)
|
|
||||||
.direction(DIRECTION_OUT)
|
|
||||||
.requestTime(requestTime)
|
|
||||||
.responseTime(ZonedDateTime.now(clock))
|
|
||||||
.businessType(businessType)
|
|
||||||
.throwable(throwable)
|
|
||||||
.build());
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
log.error(e.toString(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void log(LogMessage logMessage) {
|
|
||||||
StringMapMessage stringMapMessage = new StringMapMessage();
|
|
||||||
addLogString(stringMapMessage, PARAM_REQUEST_HEADERS,
|
|
||||||
toHeaderString(logMessage.requestHeaders()));
|
|
||||||
addLogString(stringMapMessage, PARAM_RESPONSE_HEADERS,
|
|
||||||
toHeaderString(logMessage.responseHeaders()));
|
|
||||||
addLogString(stringMapMessage, PARAM_URL_FULL, logMessage.url().toString());
|
|
||||||
addLogString(stringMapMessage, PARAM_URL_DOMAIN, logMessage.url().getHost());
|
|
||||||
addLogString(stringMapMessage, PARAM_URL_EXTENSION,
|
|
||||||
extractExtension(logMessage.url().getPath()));
|
|
||||||
addLogString(stringMapMessage, PARAM_URL_PATH, logMessage.url().getPath());
|
|
||||||
addLogString(stringMapMessage, PARAM_URL_PORT, Integer.toString(logMessage.url().getPort()));
|
|
||||||
addLogString(stringMapMessage, PARAM_URL_SCHEME, logMessage.url().getProtocol());
|
|
||||||
addLogString(stringMapMessage, PARAM_URL_QUERY, logMessage.url().getQuery());
|
|
||||||
addLogString(stringMapMessage, PARAM_REQUEST_METHOD, logMessage.method());
|
|
||||||
addLogString(stringMapMessage, PARAM_REQUEST_REFERER,
|
|
||||||
getHeader(logMessage.requestHeaders(), HttpHeaders.REFERER));
|
|
||||||
addLogString(stringMapMessage, PARAM_REQUEST_MIMETYPE, logMessage.requestMimeType());
|
|
||||||
addLogString(stringMapMessage, PARAM_RESPONSE_MIMETYPE, logMessage.responseMimeType());
|
|
||||||
addLogString(stringMapMessage, PARAM_REQUEST_BYTES, Integer.toString(logMessage.requestSize()));
|
|
||||||
addLogString(stringMapMessage, PARAM_RESPONSE_BYTES,
|
|
||||||
Integer.toString(logMessage.responseSize()));
|
|
||||||
addLogString(stringMapMessage, PARAM_RESPONSE_STATUS,
|
|
||||||
Integer.toString(logMessage.httpStatus()));
|
|
||||||
addLogString(stringMapMessage, PARAM_DIRECTION, logMessage.direction());
|
|
||||||
addLogString(stringMapMessage, PARAM_PROTOCOL, PROTOCOL_NAME);
|
|
||||||
addLogString(stringMapMessage, PARAM_REQUEST_TIME,
|
|
||||||
logMessage.requestTime().format(DATE_TIME_FORMATTER));
|
|
||||||
addLogString(stringMapMessage, PARAM_RESPONSE_TIME,
|
|
||||||
logMessage.responseTime().format(DATE_TIME_FORMATTER));
|
|
||||||
addLogString(stringMapMessage, PARAM_DURATION,
|
|
||||||
Long.toString(getDurationBetweenRequestAndResponseTime(logMessage).toNanos()));
|
|
||||||
addLogString(stringMapMessage, PARAM_USER_AGENT,
|
|
||||||
getHeader(logMessage.requestHeaders(), HttpHeaders.USER_AGENT));
|
|
||||||
addLogString(stringMapMessage, HeaderInterceptor.LOGGER_TRACE_ID, logMessage.traceId());
|
|
||||||
addLogString(stringMapMessage, HeaderInterceptor.LOGGER_REQTYPE_ID, logMessage.requestType());
|
|
||||||
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, logMessage.throwable());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Duration getDurationBetweenRequestAndResponseTime(LogMessage logMessage) {
|
|
||||||
return Duration.between(logMessage.requestTime(), logMessage.responseTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getHeader(Map<String, Collection<String>> headers, String headerKey) {
|
|
||||||
return headers.entrySet().stream()
|
|
||||||
.filter(e -> e.getKey().equalsIgnoreCase(headerKey))
|
|
||||||
.flatMap(e -> e.getValue().stream())
|
|
||||||
.findAny()
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addLogString(StringMapMessage stringMapMessage, String key, String value) {
|
|
||||||
if (value != null && !value.isBlank()) {
|
|
||||||
stringMapMessage.with(key, value.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Collection<String>> extractHeaders(Iterator<String> headerNames,
|
|
||||||
Function<String, Iterator<String>> headerValuesSupplier) {
|
|
||||||
Map<String, Collection<String>> requestHeaders = new HashMap<>();
|
|
||||||
while (headerNames.hasNext()) {
|
|
||||||
String name = headerNames.next();
|
|
||||||
Collection<String> values = requestHeaders.computeIfAbsent(name, n -> new TreeSet<>());
|
|
||||||
Iterator<String> headerValues = headerValuesSupplier.apply(name);
|
|
||||||
while (headerValues.hasNext()) {
|
|
||||||
values.add(headerValues.next());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return requestHeaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Collection<String>> extractHeaders(HttpHeaders headers) {
|
|
||||||
Map<String, Collection<String>> result = new HashMap<>();
|
|
||||||
for (Entry<String, List<String>> entry : headers.entrySet()) {
|
|
||||||
result.put(entry.getKey(), new ArrayList<>(entry.getValue()));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String toHeaderString(Map<String, Collection<String>> headerMap) {
|
|
||||||
return headerMap.entrySet().stream()
|
|
||||||
.flatMap(es -> es.getValue().stream().map(v -> new KeyValuePair(es.getKey(), v)))
|
|
||||||
.map(kv -> kv.key() + "=" + kv.value())
|
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String typeToString(String contentType) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(contentType)
|
|
||||||
.map(MediaType::parseMediaType)
|
|
||||||
.map(LoggingInterceptorRest::typeToString)
|
|
||||||
.orElse(null);
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
log.info(e.toString(), e);
|
|
||||||
return e.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String typeToString(MediaType mediaType) {
|
|
||||||
try {
|
|
||||||
return Optional.ofNullable(mediaType)
|
|
||||||
.map(m -> m.getType() + "/" + m.getSubtype())
|
|
||||||
.orElse(null);
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
log.info(e.toString(), e);
|
|
||||||
return e.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractExtension(String fileName) {
|
|
||||||
return Optional.ofNullable(fileName)
|
|
||||||
.filter(name -> name.contains("."))
|
|
||||||
.map(name -> name.substring(name.lastIndexOf('.') + 1))
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String cutToMaxLength(String string) {
|
|
||||||
if (string != null && string.length() > MAX_LOG_SIZE) {
|
|
||||||
return string.substring(0, MAX_LOG_SIZE);
|
|
||||||
}
|
|
||||||
return string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* usually returns null, but can be overridden to implement more complex logic
|
|
||||||
*/
|
|
||||||
public String determineBusinessType(URL requestUrl, String requestBody) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* usually returns UTF-8, but can be overridden to implement more complex logic
|
|
||||||
*/
|
|
||||||
public Charset determineRequestEncoding(Map<String, Collection<String>> requestHeaders,
|
|
||||||
byte[] fullRequest) {
|
|
||||||
return StandardCharsets.UTF_8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* usually returns UTF-8, but can be overridden to implement more complex logic
|
|
||||||
*/
|
|
||||||
public Charset determineResponseEncoding(Map<String, Collection<String>> responseHeaders,
|
|
||||||
byte[] fullResponse) {
|
|
||||||
return StandardCharsets.UTF_8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
private record LogMessage(Map<String, Collection<String>> requestHeaders,
|
|
||||||
Map<String, Collection<String>> responseHeaders, URL url, String method,
|
|
||||||
String requestMimeType, String responseMimeType, String requestBody,
|
|
||||||
String responseBody,
|
|
||||||
int requestSize, int responseSize, int httpStatus, String direction,
|
|
||||||
ZonedDateTime requestTime, ZonedDateTime responseTime, String traceId,
|
|
||||||
String requestType,
|
|
||||||
String businessType, Throwable throwable) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private record KeyValuePair(String key, String value) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum FieldLogBehaviour {
|
|
||||||
NEVER, ONLY_ON_ERROR, ALWAYS
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.db.Candidate;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface CandidateMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Candidate mapToExternal(Candidate candidate);
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.internal.Candidate mapToInternal(
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Candidate candidate);
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.internal.Candidate mapToInternal(Candidate candidate);
|
|
||||||
|
|
||||||
@Mapping(target = "id", ignore = true)
|
|
||||||
@Mapping(target = "created", ignore = true)
|
|
||||||
@Mapping(target = "lastModified", ignore = true)
|
|
||||||
Candidate mapToDB(de.twomartens.wahlrecht.model.dto.v1.Candidate candidate);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.db.Constituency;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface ConstituencyMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Constituency mapToExternal(Constituency constituency);
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.internal.Constituency mapToInternal(
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Constituency constituency);
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.internal.Constituency mapToInternal(Constituency constituency);
|
|
||||||
|
|
||||||
@Mapping(target = "id", ignore = true)
|
|
||||||
@Mapping(target = "created", ignore = true)
|
|
||||||
@Mapping(target = "lastModified", ignore = true)
|
|
||||||
Constituency mapToDB(de.twomartens.wahlrecht.model.dto.v1.Constituency constituency);
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectedCandidate;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface ElectedCandidateMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate mapToExternal(ElectedCandidate candidate);
|
|
||||||
|
|
||||||
ElectedCandidate mapToInternal(de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate candidate);
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectedCandidate;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectedCandidates;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.VotingResult;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface ElectedCandidatesMapper {
|
|
||||||
|
|
||||||
ElectedResultMapper ELECTED_RESULT_MAPPER = Mappers.getMapper(ElectedResultMapper.class);
|
|
||||||
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.ElectedCandidates mapToExternal(ElectedCandidates candidate);
|
|
||||||
|
|
||||||
default Map<String, Collection<de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate>> mapToExternal(
|
|
||||||
Map<VotingResult, Collection<ElectedCandidate>> value) {
|
|
||||||
return ELECTED_RESULT_MAPPER.mapToExternal(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.internal.Elected;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface ElectedMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Elected mapToExternal(Elected elected);
|
|
||||||
|
|
||||||
Elected mapToInternal(de.twomartens.wahlrecht.model.dto.v1.Elected elected);
|
|
||||||
}
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectedResult;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface ElectedResultMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.ElectedResult mapToExternal(ElectedResult result);
|
|
||||||
|
|
||||||
default Map<String, Collection<ElectedCandidate>> mapToExternal(
|
|
||||||
@NonNull Map<de.twomartens.wahlrecht.model.internal.VotingResult,
|
|
||||||
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> value) {
|
|
||||||
return value.entrySet().stream()
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
entry -> entry.getKey().getNominationId().partyAbbreviation(),
|
|
||||||
entry -> mapToExternal(entry.getValue())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Collection<ElectedCandidate> mapToExternal(
|
|
||||||
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate> value);
|
|
||||||
|
|
||||||
default Map<de.twomartens.wahlrecht.model.internal.NominationId,
|
|
||||||
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> map(
|
|
||||||
@NonNull Map<de.twomartens.wahlrecht.model.internal.VotingResult,
|
|
||||||
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> value) {
|
|
||||||
return value.entrySet().stream()
|
|
||||||
.collect(Collectors.toMap(entry -> entry.getKey().getNominationId(), Entry::getValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.db.Election;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface ElectionMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Election mapToExternal(Election election);
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.internal.Election mapToInternal(
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Election election);
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.internal.Election mapToInternal(Election election);
|
|
||||||
|
|
||||||
@Mapping(target = "id", ignore = true)
|
|
||||||
@Mapping(target = "created", ignore = true)
|
|
||||||
@Mapping(target = "lastModified", ignore = true)
|
|
||||||
Election mapToDB(de.twomartens.wahlrecht.model.dto.v1.Election election);
|
|
||||||
}
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectionResult;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.VotingResult;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface ElectionResultMapper {
|
|
||||||
|
|
||||||
VotingResultMapper VOTING_RESULT_MAPPER = Mappers.getMapper(VotingResultMapper.class);
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.ElectionResult mapToExternal(
|
|
||||||
de.twomartens.wahlrecht.model.db.ElectionResult result);
|
|
||||||
|
|
||||||
Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult> mapToExternal(
|
|
||||||
Collection<de.twomartens.wahlrecht.model.db.VotingResult> results);
|
|
||||||
|
|
||||||
Map<Integer, Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>> mapToExternal(
|
|
||||||
Map<Integer, Collection<de.twomartens.wahlrecht.model.db.VotingResult>> results);
|
|
||||||
|
|
||||||
@Mapping(target = "id", ignore = true)
|
|
||||||
@Mapping(target = "created", ignore = true)
|
|
||||||
@Mapping(target = "lastModified", ignore = true)
|
|
||||||
de.twomartens.wahlrecht.model.db.ElectionResult mapToDB(de.twomartens.wahlrecht.model.dto.v1.ElectionResult result);
|
|
||||||
|
|
||||||
Collection<de.twomartens.wahlrecht.model.db.VotingResult> mapToDB(
|
|
||||||
Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult> results);
|
|
||||||
|
|
||||||
Map<Integer, Collection<de.twomartens.wahlrecht.model.db.VotingResult>> mapToDB(
|
|
||||||
Map<Integer, Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>> results);
|
|
||||||
|
|
||||||
ElectionResult mapToInternal(de.twomartens.wahlrecht.model.dto.v1.ElectionResult result);
|
|
||||||
|
|
||||||
default VotingResult mapToInternal(de.twomartens.wahlrecht.model.dto.v1.VotingResult result) {
|
|
||||||
return VOTING_RESULT_MAPPER.mapToInternal(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Collection<VotingResult> mapToInternal(
|
|
||||||
Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult> results);
|
|
||||||
|
|
||||||
Map<Integer, Collection<VotingResult>> mapToInternal(
|
|
||||||
Map<Integer, Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>> results);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.dto.v1.NominationId;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface NominationIdMapper {
|
|
||||||
NominationId mapToExternal(de.twomartens.wahlrecht.model.internal.NominationId id);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.db.Nomination;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.Candidate;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.NominationId;
|
|
||||||
import java.util.Collection;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface NominationMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Nomination mapToExternal(Nomination nomination);
|
|
||||||
|
|
||||||
default de.twomartens.wahlrecht.model.internal.Nomination mapToInternal(
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.Nomination nomination) {
|
|
||||||
de.twomartens.wahlrecht.model.internal.Nomination result =
|
|
||||||
new de.twomartens.wahlrecht.model.internal.Nomination(
|
|
||||||
new NominationId(nomination.getElectionName(), nomination.getPartyAbbreviation(),
|
|
||||||
nomination.getName()),
|
|
||||||
nomination.isSupportVotesOnNomination()
|
|
||||||
);
|
|
||||||
result.addCandidates(mapFromDto(nomination.getCandidates()));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
default de.twomartens.wahlrecht.model.internal.Nomination mapToInternal(Nomination nomination) {
|
|
||||||
de.twomartens.wahlrecht.model.internal.Nomination result =
|
|
||||||
new de.twomartens.wahlrecht.model.internal.Nomination(
|
|
||||||
new NominationId(nomination.getElectionName(), nomination.getPartyAbbreviation(),
|
|
||||||
nomination.getName()),
|
|
||||||
nomination.isSupportVotesOnNomination()
|
|
||||||
);
|
|
||||||
result.addCandidates(mapFromDb(nomination.getCandidates()));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mapping(target = "id", ignore = true)
|
|
||||||
@Mapping(target = "created", ignore = true)
|
|
||||||
@Mapping(target = "lastModified", ignore = true)
|
|
||||||
Nomination mapToDB(de.twomartens.wahlrecht.model.dto.v1.Nomination nomination);
|
|
||||||
|
|
||||||
Collection<Candidate> mapFromDb(Collection<de.twomartens.wahlrecht.model.db.Candidate> candidates);
|
|
||||||
|
|
||||||
Collection<Candidate> mapFromDto(Collection<de.twomartens.wahlrecht.model.dto.v1.Candidate> candidates);
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.db.PartyInElection;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface PartyInElectionMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.PartyInElection mapToExternal(PartyInElection election);
|
|
||||||
|
|
||||||
@Mapping(target = "id", ignore = true)
|
|
||||||
@Mapping(target = "created", ignore = true)
|
|
||||||
@Mapping(target = "lastModified", ignore = true)
|
|
||||||
PartyInElection mapToDB(de.twomartens.wahlrecht.model.dto.v1.PartyInElection election);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.internal.SeatResult;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface SeatResultMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.SeatResult mapToExternal(SeatResult result);
|
|
||||||
|
|
||||||
SeatResult mapToInternal(de.twomartens.wahlrecht.model.dto.v1.SeatResult result);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.internal.NominationId;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.VotingResult;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface VotingResultMapper {
|
|
||||||
|
|
||||||
default de.twomartens.wahlrecht.model.dto.v1.VotingResult mapToExternal(@NonNull VotingResult result) {
|
|
||||||
return new de.twomartens.wahlrecht.model.dto.v1.VotingResult(
|
|
||||||
result.getNominationId().electionName(),
|
|
||||||
result.getNominationId().partyAbbreviation(),
|
|
||||||
result.getNominationId().name(),
|
|
||||||
result.getVotesOnNomination(),
|
|
||||||
result.getVotesThroughHealing(),
|
|
||||||
result.getVotesPerPosition()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
default VotingResult mapToInternal(@NonNull de.twomartens.wahlrecht.model.dto.v1.VotingResult result) {
|
|
||||||
return new VotingResult(
|
|
||||||
new NominationId(result.electionName(), result.partyAbbreviation(),
|
|
||||||
result.nominationName()),
|
|
||||||
result.votesOnNomination(),
|
|
||||||
result.votesThroughHealing(),
|
|
||||||
result.votesPerPosition()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.mapper.v1;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.internal.VotingThreshold;
|
|
||||||
import org.mapstruct.CollectionMappingStrategy;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.NullValueCheckStrategy;
|
|
||||||
import org.mapstruct.ReportingPolicy;
|
|
||||||
|
|
||||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
|
||||||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
public interface VotingThresholdMapper {
|
|
||||||
|
|
||||||
de.twomartens.wahlrecht.model.dto.v1.VotingThreshold mapToExternal(VotingThreshold threshold);
|
|
||||||
|
|
||||||
VotingThreshold mapToInternal(de.twomartens.wahlrecht.model.dto.v1.VotingThreshold threshold);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.db;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
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.CompoundIndex;
|
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
|
||||||
|
|
||||||
@Document
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@EqualsAndHashCode
|
|
||||||
@ToString
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
|
|
||||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
|
||||||
@CompoundIndex(def = "{'name': 1, 'partyAbbreviation': 1, 'electionName': 1}")
|
|
||||||
public class Nomination {
|
|
||||||
|
|
||||||
@EqualsAndHashCode.Exclude
|
|
||||||
@Id
|
|
||||||
ObjectId id;
|
|
||||||
@EqualsAndHashCode.Exclude
|
|
||||||
@CreatedDate
|
|
||||||
Instant created;
|
|
||||||
@EqualsAndHashCode.Exclude
|
|
||||||
@LastModifiedDate
|
|
||||||
Instant lastModified;
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String partyAbbreviation;
|
|
||||||
String electionName;
|
|
||||||
Collection<Candidate> candidates;
|
|
||||||
boolean supportVotesOnNomination;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto;
|
|
||||||
|
|
||||||
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 ErrorMessage {
|
|
||||||
|
|
||||||
String message;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
public record Candidate(String name, String profession) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
public record Constituency(String electionName, int number, String name, int numberOfSeats) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
public enum Elected {
|
|
||||||
NOT_ELECTED,
|
|
||||||
CONSTITUENCY,
|
|
||||||
OVERALL_NOMINATION_ORDER,
|
|
||||||
OVERALL_VOTE_ORDER
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
public record ElectedCandidate(Candidate candidate, Elected elected) {
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record ElectedCandidates(ElectedResult overallResult,
|
|
||||||
Map<Integer, ElectedResult> constituencyResults,
|
|
||||||
LinkedList<Double> electionNumbersForSeatAllocation) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.Builder;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
public record ElectedResult(Map<String, Collection<ElectedCandidate>> electedCandidates,
|
|
||||||
LinkedList<Double> usedElectionNumbers) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public record Election(String name, LocalDate day, VotingThreshold votingThreshold,
|
|
||||||
int totalNumberOfSeats,
|
|
||||||
Collection<Constituency> constituencies) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record ElectionResult(String electionName, Collection<VotingResult> overallResults,
|
|
||||||
Map<Integer, Collection<VotingResult>> constituencyResults) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@EqualsAndHashCode
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class Nomination {
|
|
||||||
|
|
||||||
String electionName;
|
|
||||||
String partyAbbreviation;
|
|
||||||
String name;
|
|
||||||
boolean supportVotesOnNomination;
|
|
||||||
List<Candidate> candidates;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
public record NominationId(String electionName, String partyAbbreviation, String name) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.ToString.Include;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@EqualsAndHashCode
|
|
||||||
@ToString(onlyExplicitlyIncluded = true)
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class PartyInElection {
|
|
||||||
|
|
||||||
String electionName;
|
|
||||||
@Include
|
|
||||||
String abbreviation;
|
|
||||||
String name;
|
|
||||||
Nomination overallNomination;
|
|
||||||
@NonNull
|
|
||||||
Map<Integer, Nomination> constituencyNominations = new HashMap<>();
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.Builder;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
public record SeatResult(Map<VotingResult, Integer> seatsPerResult,
|
|
||||||
LinkedList<Double> usedElectionNumbers) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record VotingResult(String electionName, String partyAbbreviation, String nominationName,
|
|
||||||
int votesOnNomination, int votesThroughHealing,
|
|
||||||
Map<Integer, Integer> votesPerPosition) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.dto.v1;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
|
||||||
public enum VotingThreshold {
|
|
||||||
NONE(0.00),
|
|
||||||
THREE(0.03),
|
|
||||||
FIVE(0.05);
|
|
||||||
|
|
||||||
private final double multiplier;
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.internal;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.Builder;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
public record ElectedCandidates(ElectedResult overallResult,
|
|
||||||
Map<Integer, ElectedResult> constituencyResults,
|
|
||||||
LinkedList<Double> electionNumbersForSeatAllocation) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.internal;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.Builder;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
public record ElectedResult(Map<VotingResult, Collection<ElectedCandidate>> electedCandidates,
|
|
||||||
LinkedList<Double> usedElectionNumbers) {
|
|
||||||
|
|
||||||
public Map<NominationId, Collection<ElectedCandidate>> electedCandidatesByNomination() {
|
|
||||||
return electedCandidates.entrySet().stream()
|
|
||||||
.collect(Collectors.toMap(entry -> entry.getKey().getNominationId(), Entry::getValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.internal;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public record Election(String name, LocalDate day, VotingThreshold votingThreshold,
|
|
||||||
int totalNumberOfSeats,
|
|
||||||
Collection<Constituency> constituencies) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.internal;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class Nomination {
|
|
||||||
@Getter
|
|
||||||
private final NominationId id;
|
|
||||||
private final boolean supportVotesOnNomination;
|
|
||||||
private final List<Candidate> candidates = new ArrayList<>();
|
|
||||||
private final Map<Candidate, Integer> candidateToPosition = new HashMap<>();
|
|
||||||
private final Map<Integer, Candidate> positionToCandidate = new HashMap<>();
|
|
||||||
|
|
||||||
private void addCandidate(Candidate candidate) {
|
|
||||||
candidates.add(candidate);
|
|
||||||
candidateToPosition.put(candidate, candidates.size());
|
|
||||||
positionToCandidate.put(candidates.size(), candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addCandidates(Candidate... candidates) {
|
|
||||||
Arrays.stream(candidates).forEach(this::addCandidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addCandidates(Collection<Candidate> candidates) {
|
|
||||||
candidates.forEach(this::addCandidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Candidate> getCandidates() {
|
|
||||||
return new ArrayList<>(positionToCandidate.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Candidate getCandidate(int position) {
|
|
||||||
return positionToCandidate.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPosition(Candidate candidate) {
|
|
||||||
return candidateToPosition.get(candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean supportsVotesOnNomination() {
|
|
||||||
return supportVotesOnNomination;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return id.name();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.internal;
|
|
||||||
|
|
||||||
public record NominationId(String electionName, String partyAbbreviation, String name) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.internal;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.Builder;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
public record SeatResult(Map<VotingResult, Integer> seatsPerResult,
|
|
||||||
LinkedList<Double> usedElectionNumbers) {
|
|
||||||
|
|
||||||
public Map<NominationId, Integer> seatsPerNomination() {
|
|
||||||
return seatsPerResult.entrySet().stream()
|
|
||||||
.collect(Collectors.toMap(entry -> entry.getKey().getNominationId(), Entry::getValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.internal;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class VotingResult {
|
|
||||||
|
|
||||||
private final NominationId nominationId;
|
|
||||||
private final int votesOnNomination;
|
|
||||||
private final int votesThroughHealing;
|
|
||||||
private final Map<Integer, Integer> votesPerPosition;
|
|
||||||
private Integer totalVotes = null;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
public VotingResult(NominationId nominationId, int votesOnNomination, int votesThroughHealing,
|
|
||||||
Map<Integer, Integer> votesPerPosition) {
|
|
||||||
this.votesOnNomination = votesOnNomination;
|
|
||||||
this.votesThroughHealing = votesThroughHealing;
|
|
||||||
this.votesPerPosition = votesPerPosition;
|
|
||||||
this.nominationId = nominationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalVotes() {
|
|
||||||
if (totalVotes == null) {
|
|
||||||
totalVotes = votesOnNomination + votesThroughHealing + votesPerPosition.values().stream()
|
|
||||||
.reduce(0, Integer::sum);
|
|
||||||
}
|
|
||||||
return totalVotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalVotesWithoutHealing() {
|
|
||||||
return getTotalVotes() - votesThroughHealing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.model.internal;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
|
||||||
public enum VotingThreshold {
|
|
||||||
NONE(0.00),
|
|
||||||
THREE(0.03),
|
|
||||||
FIVE(0.05);
|
|
||||||
|
|
||||||
private final double multiplier;
|
|
||||||
}
|
|
||||||
+4
-3
@@ -1,9 +1,6 @@
|
|||||||
package de.twomartens.wahlrecht.monitoring.actuator;
|
package de.twomartens.wahlrecht.monitoring.actuator;
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest;
|
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.time.Clock;
|
|
||||||
import java.util.Optional;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.actuate.health.Health;
|
import org.springframework.boot.actuate.health.Health;
|
||||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||||
@@ -13,6 +10,10 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Health check which checks if the rest services are working.
|
* A Health check which checks if the rest services are working.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.property;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.boot.convert.DurationUnit;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
@EqualsAndHashCode
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@ConfigurationProperties(prefix = "restwahlrecht.timeout")
|
|
||||||
@Component
|
|
||||||
public class RestTemplateTimeoutProperties {
|
|
||||||
|
|
||||||
@DurationUnit(ChronoUnit.MILLIS)
|
|
||||||
private Duration readTimeoutRestHealthIndicatorInMillis;
|
|
||||||
@DurationUnit(ChronoUnit.MILLIS)
|
|
||||||
private Duration connectionRestHealthIndicatorTimeoutInMillis;
|
|
||||||
@DurationUnit(ChronoUnit.MILLIS)
|
|
||||||
private Duration readTimeoutRestTemplateInMillis;
|
|
||||||
@DurationUnit(ChronoUnit.MILLIS)
|
|
||||||
private Duration connectionRestTemplateTimeoutInMillis;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.property;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@RefreshScope
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties(prefix = "de.twomartens.wahlrecht")
|
|
||||||
@Schema(description = "Properties, to configure this Application")
|
|
||||||
public class ServiceProperties {
|
|
||||||
|
|
||||||
private ZoneId defaultTimeZone;
|
|
||||||
|
|
||||||
private String greeting;
|
|
||||||
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.property;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.boot.convert.DurationUnit;
|
|
||||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@RefreshScope
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties(prefix = "de.twomartens.wahlrecht.statusprobe")
|
|
||||||
@Schema(description = "Properties, to configure this Application")
|
|
||||||
public class StatusProbeProperties {
|
|
||||||
|
|
||||||
@DurationUnit(ChronoUnit.SECONDS)
|
|
||||||
private Duration scheduleDuration;
|
|
||||||
|
|
||||||
@DurationUnit(ChronoUnit.MINUTES)
|
|
||||||
private Duration maxKafkaFailureDuration;
|
|
||||||
|
|
||||||
private int maxBlobFailureCount;
|
|
||||||
|
|
||||||
private int maxFailurePercent;
|
|
||||||
}
|
|
||||||
+3
-2
@@ -1,11 +1,12 @@
|
|||||||
package de.twomartens.wahlrecht.repository;
|
package de.twomartens.wahlrecht.repository;
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.db.Nomination;
|
import de.twomartens.wahlrecht.model.db.Nomination;
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface NominationRepository extends MongoRepository<Nomination, String> {
|
public interface NominationRepository extends MongoRepository<Nomination, String> {
|
||||||
|
|
||||||
Optional<Nomination> findByNameAndElectionNameAndPartyAbbreviation(String name,
|
Optional<Nomination> findByNameAndElectionNameAndPartyAbbreviation(String name,
|
||||||
String electionName, String partyAbbreviation);
|
String electionName, String partyAbbreviation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,323 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.service;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.model.internal.Candidate;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.Constituency;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.Elected;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectedCandidate;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectedCandidates;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectedResult;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.Election;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.ElectionResult;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.Nomination;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.NominationId;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.SeatResult;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.VotingResult;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.StopWatch;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Service
|
|
||||||
@Slf4j
|
|
||||||
public class CalculationService {
|
|
||||||
|
|
||||||
private final NominationService nominationService;
|
|
||||||
private final ElectionService electionService;
|
|
||||||
private final LinkedList<Double> electionNumberHistory = new LinkedList<>();
|
|
||||||
|
|
||||||
public ElectedCandidates determineElectedCandidates(@NonNull ElectionResult electionResult) {
|
|
||||||
log.info("Calculate election result for election {}", electionResult.electionName());
|
|
||||||
StopWatch stopWatch = new StopWatch();
|
|
||||||
stopWatch.start("determineElectedCandidates");
|
|
||||||
Election election = electionService.getElectionInternal(electionResult.electionName());
|
|
||||||
SeatResult seatResult = calculateOverallSeatDistribution(election,
|
|
||||||
electionResult.overallResults());
|
|
||||||
Map<Integer, ElectedResult> constituencyResults = new HashMap<>();
|
|
||||||
|
|
||||||
for (Constituency constituency : election.constituencies()) {
|
|
||||||
ElectedResult electedResult = calculateConstituency(constituency,
|
|
||||||
electionResult.constituencyResults().get(constituency.number()));
|
|
||||||
constituencyResults.put(constituency.number(), electedResult);
|
|
||||||
}
|
|
||||||
Map<String, Collection<ElectedCandidate>> electedCandidatesMap = constituencyResults
|
|
||||||
.entrySet().stream()
|
|
||||||
.flatMap(entry -> entry.getValue().electedCandidates().entrySet().stream())
|
|
||||||
.collect(Collectors.groupingBy(entry -> entry.getKey().getNominationId().partyAbbreviation(),
|
|
||||||
Collectors.flatMapping(entry -> entry.getValue().stream(),
|
|
||||||
Collectors.toCollection(ArrayList::new))));
|
|
||||||
|
|
||||||
Map<VotingResult, Integer> remainingSeatsPerParty = seatResult.seatsPerResult()
|
|
||||||
.entrySet().stream()
|
|
||||||
.map(entry -> Map.entry(entry.getKey(),
|
|
||||||
entry.getValue() - electedCandidatesMap.getOrDefault(
|
|
||||||
entry.getKey().getNominationId().partyAbbreviation(),
|
|
||||||
Collections.emptyList()).size()))
|
|
||||||
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
|
||||||
|
|
||||||
ElectedResult overallElectedCandidates = calculateElectedOverallCandidates(
|
|
||||||
remainingSeatsPerParty,
|
|
||||||
electedCandidatesMap
|
|
||||||
);
|
|
||||||
|
|
||||||
stopWatch.stop();
|
|
||||||
log.debug("determineCandidates took {} seconds", stopWatch.getTotalTimeSeconds());
|
|
||||||
|
|
||||||
return ElectedCandidates.builder()
|
|
||||||
.overallResult(overallElectedCandidates)
|
|
||||||
.constituencyResults(constituencyResults)
|
|
||||||
.electionNumbersForSeatAllocation(seatResult.usedElectionNumbers())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ElectedResult calculateConstituency(@NonNull Constituency constituency,
|
|
||||||
@NonNull Collection<VotingResult> votingResults) {
|
|
||||||
electionNumberHistory.clear();
|
|
||||||
log.info("Calculate constituency {}", constituency.name());
|
|
||||||
int totalVotes = votingResults.stream()
|
|
||||||
.map(VotingResult::getTotalVotes)
|
|
||||||
.reduce(0, Integer::sum);
|
|
||||||
int numberOfSeats = constituency.numberOfSeats();
|
|
||||||
double initialElectionNumber = totalVotes / (double) numberOfSeats;
|
|
||||||
|
|
||||||
Map<VotingResult, Integer> assignedSeatsPerResult = calculateAssignedSeatsPerResult(
|
|
||||||
votingResults, numberOfSeats, initialElectionNumber);
|
|
||||||
|
|
||||||
return ElectedResult.builder()
|
|
||||||
.electedCandidates(findElectedCandidates(assignedSeatsPerResult, Collections.emptyMap()))
|
|
||||||
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SeatResult calculateOverallSeatDistribution(@NonNull Election election,
|
|
||||||
@NonNull Collection<VotingResult> votingResults) {
|
|
||||||
log.info("Calculate overall seat distribution for election {}", election.name());
|
|
||||||
electionNumberHistory.clear();
|
|
||||||
int totalVotes = votingResults.stream()
|
|
||||||
.map(VotingResult::getTotalVotes)
|
|
||||||
.reduce(0, Integer::sum);
|
|
||||||
|
|
||||||
List<VotingResult> validVotingResults = new ArrayList<>();
|
|
||||||
int totalIgnoredVotes = 0;
|
|
||||||
for (VotingResult votingResult : votingResults) {
|
|
||||||
if (passesVotingThreshold(election, totalVotes, votingResult)) {
|
|
||||||
log.info("Party passes voting threshold: {}",
|
|
||||||
votingResult.getNominationId().partyAbbreviation());
|
|
||||||
validVotingResults.add(votingResult);
|
|
||||||
} else {
|
|
||||||
log.info("Party fails voting threshold: {}",
|
|
||||||
votingResult.getNominationId().partyAbbreviation());
|
|
||||||
totalIgnoredVotes += votingResult.getTotalVotes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalVotes -= totalIgnoredVotes;
|
|
||||||
|
|
||||||
int numberOfSeats = election.totalNumberOfSeats();
|
|
||||||
double initialElectionNumber = totalVotes / (double) numberOfSeats;
|
|
||||||
|
|
||||||
Map<VotingResult, Integer> assignedSeatsPerResult = calculateAssignedSeatsPerResult(
|
|
||||||
validVotingResults, numberOfSeats, initialElectionNumber);
|
|
||||||
|
|
||||||
return SeatResult.builder()
|
|
||||||
.seatsPerResult(assignedSeatsPerResult)
|
|
||||||
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all elected candidates for given voting results
|
|
||||||
*
|
|
||||||
* @param seatsPerNomination Seats to allocate per voting result
|
|
||||||
* @param electedCandidates Already elected candidates per party
|
|
||||||
*/
|
|
||||||
public ElectedResult calculateElectedOverallCandidates(
|
|
||||||
Map<VotingResult, Integer> seatsPerNomination,
|
|
||||||
Map<String, Collection<ElectedCandidate>> electedCandidates) {
|
|
||||||
log.info("Calculate overall elected candidates");
|
|
||||||
electionNumberHistory.clear();
|
|
||||||
|
|
||||||
return ElectedResult.builder()
|
|
||||||
.electedCandidates(findElectedCandidates(seatsPerNomination, electedCandidates))
|
|
||||||
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean passesVotingThreshold(@NonNull Election election, int totalVotes,
|
|
||||||
@NonNull VotingResult votingResult) {
|
|
||||||
return totalVotes * election.votingThreshold().getMultiplier()
|
|
||||||
<= votingResult.getTotalVotes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all elected candidates for given voting results.
|
|
||||||
*
|
|
||||||
* @param assignedSeatsPerNomination Map of seats to allocate for a voting result
|
|
||||||
* @param alreadyElectedCandidates Map of already elected candidates per party
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
private Map<VotingResult, Collection<ElectedCandidate>> findElectedCandidates(
|
|
||||||
@NonNull Map<VotingResult, Integer> assignedSeatsPerNomination,
|
|
||||||
Map<String, Collection<ElectedCandidate>> alreadyElectedCandidates) {
|
|
||||||
|
|
||||||
Map<VotingResult, Collection<ElectedCandidate>> electedCandidates = new HashMap<>();
|
|
||||||
for (Entry<VotingResult, Integer> entry : assignedSeatsPerNomination.entrySet()) {
|
|
||||||
if (entry.getValue() > 0) {
|
|
||||||
Collection<ElectedCandidate> candidates = findCandidates(
|
|
||||||
entry.getKey(),
|
|
||||||
entry.getValue(),
|
|
||||||
alreadyElectedCandidates.getOrDefault(
|
|
||||||
entry.getKey().getNominationId().partyAbbreviation(),
|
|
||||||
Collections.emptyList())
|
|
||||||
);
|
|
||||||
electedCandidates.put(entry.getKey(), candidates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return electedCandidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds elected candidates for given voting result.
|
|
||||||
*
|
|
||||||
* @param votingResult Result to search candidates for
|
|
||||||
* @param numberOfSeats Number of seats to allocate
|
|
||||||
* @param alreadyElectedCandidates Candidates already elected previously
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
private Collection<ElectedCandidate> findCandidates(@NonNull VotingResult votingResult,
|
|
||||||
int numberOfSeats, @NonNull Collection<ElectedCandidate> alreadyElectedCandidates) {
|
|
||||||
log.info("Find elected candidates on nomination: {}",
|
|
||||||
votingResult.getNominationId().name());
|
|
||||||
List<Integer> individualVotesOrder = votingResult.getVotesPerPosition()
|
|
||||||
.entrySet().stream()
|
|
||||||
.sorted(Comparator.comparing(Entry<Integer, Integer>::getValue).reversed())
|
|
||||||
.map(Entry::getKey)
|
|
||||||
.toList();
|
|
||||||
Collection<ElectedCandidate> elected = new ArrayList<>();
|
|
||||||
int seatsByVoteOrder = numberOfSeats;
|
|
||||||
|
|
||||||
Map<Candidate, ElectedCandidate> electedCandidateMap = alreadyElectedCandidates.stream()
|
|
||||||
.collect(Collectors.toMap(ElectedCandidate::candidate, Function.identity()));
|
|
||||||
|
|
||||||
NominationId nominationId = votingResult.getNominationId();
|
|
||||||
Nomination nomination = nominationService.getNominationInternal(nominationId);
|
|
||||||
if (nomination.supportsVotesOnNomination()) {
|
|
||||||
int seatsByNomination = calculateSeatsByNominationOrder(votingResult, numberOfSeats);
|
|
||||||
seatsByVoteOrder = numberOfSeats - seatsByNomination;
|
|
||||||
List<Candidate> candidates = nomination.getCandidates();
|
|
||||||
int electedByNominationOrder = 0;
|
|
||||||
for (int i = 0; i < candidates.size() && electedByNominationOrder < seatsByNomination; i++) {
|
|
||||||
Candidate candidate = candidates.get(i);
|
|
||||||
ElectedCandidate electedCandidate = electedCandidateMap.get(candidate);
|
|
||||||
if (electedCandidate == null) {
|
|
||||||
electedCandidate = new ElectedCandidate(candidate, Elected.OVERALL_NOMINATION_ORDER);
|
|
||||||
elected.add(electedCandidate);
|
|
||||||
electedCandidateMap.put(candidate, electedCandidate);
|
|
||||||
electedByNominationOrder++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int electedByVoteOrder = 0;
|
|
||||||
for (int i = 0; i < individualVotesOrder.size() && electedByVoteOrder < seatsByVoteOrder; i++) {
|
|
||||||
Integer position = individualVotesOrder.get(i);
|
|
||||||
Candidate candidate = nomination.getCandidate(position);
|
|
||||||
ElectedCandidate electedCandidate = electedCandidateMap.get(candidate);
|
|
||||||
if (electedCandidate == null) {
|
|
||||||
electedCandidate = new ElectedCandidate(candidate, nomination.supportsVotesOnNomination()
|
|
||||||
? Elected.OVERALL_VOTE_ORDER
|
|
||||||
: Elected.CONSTITUENCY);
|
|
||||||
elected.add(electedCandidate);
|
|
||||||
electedByVoteOrder++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return elected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int calculateSeatsByNominationOrder(@NonNull VotingResult votingResult,
|
|
||||||
int numberOfSeats) {
|
|
||||||
int votesOnNomination = votingResult.getVotesOnNomination();
|
|
||||||
int totalVotes = votingResult.getTotalVotesWithoutHealing();
|
|
||||||
return (int) Math.round((numberOfSeats * votesOnNomination) / (double) totalVotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<VotingResult, Integer> calculateAssignedSeatsPerResult(
|
|
||||||
@NonNull Collection<VotingResult> votingResults,
|
|
||||||
int numberOfSeats, Double initialElectionNumber) {
|
|
||||||
|
|
||||||
double electionNumber = initialElectionNumber;
|
|
||||||
long assignedSeats;
|
|
||||||
Map<VotingResult, Integer> assignedSeatsPerVotingResult;
|
|
||||||
log.debug("Calculate assigned seats with initial election number {}", electionNumber);
|
|
||||||
|
|
||||||
do {
|
|
||||||
electionNumberHistory.add(electionNumber);
|
|
||||||
Map<VotingResult, Double> seatsPerVotingResult = new HashMap<>();
|
|
||||||
for (VotingResult votingResult : votingResults) {
|
|
||||||
double seatNumber = calculateAssignedSeatNumber(electionNumber,
|
|
||||||
votingResult.getTotalVotes());
|
|
||||||
seatsPerVotingResult.put(votingResult, seatNumber);
|
|
||||||
}
|
|
||||||
assignedSeatsPerVotingResult = seatsPerVotingResult.entrySet().stream()
|
|
||||||
.map(entry -> Map.entry(entry.getKey(), (int) Math.round(entry.getValue())))
|
|
||||||
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
|
||||||
assignedSeats = assignedSeatsPerVotingResult.values().stream().reduce(0, Integer::sum);
|
|
||||||
|
|
||||||
if (assignedSeats < numberOfSeats) {
|
|
||||||
// election number was too big, decrease
|
|
||||||
electionNumber = calculateLowerElectionNumber(initialElectionNumber, seatsPerVotingResult,
|
|
||||||
assignedSeatsPerVotingResult);
|
|
||||||
log.debug("Calculated lower election number {}", electionNumber);
|
|
||||||
} else if (assignedSeats > numberOfSeats) {
|
|
||||||
// election number was too small, increase
|
|
||||||
electionNumber = calculateHigherElectionNumber(initialElectionNumber, seatsPerVotingResult,
|
|
||||||
assignedSeatsPerVotingResult);
|
|
||||||
log.debug("Calculated higher election number {}", electionNumber);
|
|
||||||
}
|
|
||||||
} while (assignedSeats != numberOfSeats);
|
|
||||||
|
|
||||||
return assignedSeatsPerVotingResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double calculateHigherElectionNumber(Double initialElectionNumber,
|
|
||||||
@NonNull Map<VotingResult, Double> seatsPerVotingResult,
|
|
||||||
Map<VotingResult, Integer> assignedSeatsPerVotingResult) {
|
|
||||||
double electionNumber;
|
|
||||||
electionNumber = seatsPerVotingResult.keySet().stream()
|
|
||||||
.map(votingResult -> votingResult.getTotalVotes()
|
|
||||||
/ (assignedSeatsPerVotingResult.get(votingResult) - 0.5))
|
|
||||||
.min(Comparator.comparing(Double::doubleValue))
|
|
||||||
.orElse(initialElectionNumber);
|
|
||||||
return electionNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double calculateLowerElectionNumber(Double initialElectionNumber,
|
|
||||||
@NonNull Map<VotingResult, Double> seatsPerVotingResult,
|
|
||||||
Map<VotingResult, Integer> assignedSeatsPerVotingResult) {
|
|
||||||
double electionNumber;
|
|
||||||
electionNumber = seatsPerVotingResult.keySet().stream()
|
|
||||||
.map(votingResult -> votingResult.getTotalVotes()
|
|
||||||
/ (assignedSeatsPerVotingResult.get(votingResult) + 0.5))
|
|
||||||
.max(Comparator.comparing(Double::doubleValue))
|
|
||||||
.orElse(initialElectionNumber);
|
|
||||||
return electionNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private double calculateAssignedSeatNumber(double electionNumber, int totalVotesOfNomination) {
|
|
||||||
return totalVotesOfNomination / electionNumber;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package de.twomartens.wahlrecht.service;
|
|
||||||
|
|
||||||
import de.twomartens.wahlrecht.mapper.v1.NominationMapper;
|
|
||||||
import de.twomartens.wahlrecht.model.db.Candidate;
|
|
||||||
import de.twomartens.wahlrecht.model.db.Nomination;
|
|
||||||
import de.twomartens.wahlrecht.model.internal.NominationId;
|
|
||||||
import de.twomartens.wahlrecht.repository.NominationRepository;
|
|
||||||
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.stereotype.Service;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Service
|
|
||||||
public class NominationService {
|
|
||||||
|
|
||||||
private final NominationMapper mapper = Mappers.getMapper(NominationMapper.class);
|
|
||||||
private final NominationRepository repository;
|
|
||||||
private final CandidateService candidateService;
|
|
||||||
private Map<NominationId, Nomination> nominations;
|
|
||||||
|
|
||||||
public Nomination getNomination(NominationId id) {
|
|
||||||
if (nominations == null) {
|
|
||||||
fetchNominations();
|
|
||||||
}
|
|
||||||
return nominations.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public de.twomartens.wahlrecht.model.internal.Nomination getNominationInternal(NominationId id) {
|
|
||||||
return mapper.mapToInternal(getNomination(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Nomination storeNomination(Nomination nomination) {
|
|
||||||
if (nomination == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nominations == null) {
|
|
||||||
fetchNominations();
|
|
||||||
}
|
|
||||||
|
|
||||||
NominationId nominationId = new NominationId(nomination.getElectionName(),
|
|
||||||
nomination.getPartyAbbreviation(), nomination.getName());
|
|
||||||
Nomination existing = nominations.get(nominationId);
|
|
||||||
boolean needsUpdate = !nomination.equals(existing);
|
|
||||||
|
|
||||||
if (!needsUpdate) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
Collection<Candidate> candidates = new ArrayList<>();
|
|
||||||
nomination.getCandidates()
|
|
||||||
.forEach(candidate -> candidates.add(candidateService.storeCandidate(candidate)));
|
|
||||||
if (existing != null) {
|
|
||||||
nomination = existing;
|
|
||||||
}
|
|
||||||
nomination.setCandidates(candidates);
|
|
||||||
nomination = repository.save(nomination);
|
|
||||||
nominations.put(nominationId, nomination);
|
|
||||||
|
|
||||||
return nomination;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchNominations() {
|
|
||||||
nominations = repository.findAll().stream()
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
n -> new NominationId(n.getElectionName(), n.getPartyAbbreviation(), n.getName()),
|
|
||||||
Function.identity()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package de.twomartens.wahlrecht.configuration
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.property.ServiceProperties
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import java.time.Clock
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
open class ClockConfiguration(val serviceProperties: ServiceProperties) {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun clock(): Clock {
|
||||||
|
return Clock.system(serviceProperties.defaultTimeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package de.twomartens.wahlrecht.configuration
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest
|
||||||
|
import de.twomartens.wahlrecht.interceptor.LoggingInterceptorRest
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import java.time.Clock
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
open class InterceptorConfiguration {
|
||||||
|
@Bean
|
||||||
|
open fun loggingInterceptorRest(clock: Clock): LoggingInterceptorRest {
|
||||||
|
return LoggingInterceptorRest(clock)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun headerInterceptorRest(): HeaderInterceptorRest {
|
||||||
|
return HeaderInterceptorRest()
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package de.twomartens.wahlrecht.configuration
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.property.RestTemplateTimeoutProperties
|
||||||
|
import de.twomartens.wahlrecht.property.ServiceProperties
|
||||||
|
import de.twomartens.wahlrecht.property.StatusProbeProperties
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties(RestTemplateTimeoutProperties::class, ServiceProperties::class,
|
||||||
|
StatusProbeProperties::class)
|
||||||
|
open class PropertyConfiguration
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package de.twomartens.wahlrecht.configuration
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest
|
||||||
|
import de.twomartens.wahlrecht.interceptor.LoggingInterceptorRest
|
||||||
|
import de.twomartens.wahlrecht.property.RestTemplateTimeoutProperties
|
||||||
|
import org.springframework.boot.web.client.RestTemplateBuilder
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
open class RestTemplateConfiguration {
|
||||||
|
@Bean
|
||||||
|
open fun restTemplate(
|
||||||
|
headerInterceptorRest: HeaderInterceptorRest,
|
||||||
|
loggingInterceptor: LoggingInterceptorRest,
|
||||||
|
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
|
||||||
|
): RestTemplate {
|
||||||
|
return RestTemplateBuilder()
|
||||||
|
.additionalInterceptors(headerInterceptorRest, loggingInterceptor)
|
||||||
|
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestTemplateTimeoutInMillis)
|
||||||
|
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestTemplateInMillis)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun restTemplateRestHealthIndicator(
|
||||||
|
headerInterceptorRest: HeaderInterceptorRest,
|
||||||
|
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
|
||||||
|
): RestTemplate {
|
||||||
|
return RestTemplateBuilder()
|
||||||
|
.additionalInterceptors(headerInterceptorRest)
|
||||||
|
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestHealthIndicatorTimeoutInMillis)
|
||||||
|
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestHealthIndicatorInMillis)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package de.twomartens.wahlrecht.controller
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.exception.HttpStatusException
|
||||||
|
import de.twomartens.wahlrecht.model.dto.ErrorMessage
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
|
||||||
|
|
||||||
|
@ControllerAdvice(annotations = [RestController::class])
|
||||||
|
class ExceptionController: ResponseEntityExceptionHandler() {
|
||||||
|
@ExceptionHandler(HttpStatusException::class)
|
||||||
|
fun handleException(e: HttpStatusException): ResponseEntity<ErrorMessage> {
|
||||||
|
if (e.cause != null) {
|
||||||
|
log.info(e.cause.toString(), e.cause)
|
||||||
|
} else {
|
||||||
|
log.info(e.toString())
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(e.status)
|
||||||
|
.body(ErrorMessage(e.message!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException::class)
|
||||||
|
fun handleRuntimeException(e: RuntimeException): ResponseEntity<ErrorMessage> {
|
||||||
|
log.error("unexpected exception occurred", e)
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(ErrorMessage(message = e.message!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
package de.twomartens.wahlrecht.controller
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.util.jar.JarFile
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = ["/wahlrecht"])
|
||||||
|
class VersionHtmlController {
|
||||||
|
@GetMapping(path = ["/version"])
|
||||||
|
fun version(): String {
|
||||||
|
return "version"
|
||||||
|
}
|
||||||
|
|
||||||
|
@ModelAttribute("version")
|
||||||
|
private fun getApplicationVersion(): String {
|
||||||
|
return "${getTitle()} ${getVersion()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@ModelAttribute("footerString")
|
||||||
|
private fun getApplicationVersion(@RequestHeader("host") hostName: String): String {
|
||||||
|
return "${getTitle()} ${getVersion()} - $hostName"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTitle(): String {
|
||||||
|
val title = VersionHtmlController::class.java.getPackage().implementationTitle
|
||||||
|
return title.ifBlank {
|
||||||
|
"application"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVersion(): String {
|
||||||
|
val version = VersionHtmlController::class.java.getPackage().implementationVersion
|
||||||
|
return version.ifBlank {
|
||||||
|
"DEVELOPER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ModelAttribute("hostname")
|
||||||
|
private fun getHostname(): String {
|
||||||
|
try {
|
||||||
|
return InetAddress.getLocalHost().hostName
|
||||||
|
} catch (e: UnknownHostException) {
|
||||||
|
log.warn(e.toString(), e)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@ModelAttribute("manifest")
|
||||||
|
private fun getManifest(): Collection<String> {
|
||||||
|
try {
|
||||||
|
val location = javaClass.getProtectionDomain().codeSource.location
|
||||||
|
val jarFileName = Paths.get(location.toURI()).toString()
|
||||||
|
JarFile(jarFileName).use { jarFile ->
|
||||||
|
val entry = jarFile.getEntry(JarFile.MANIFEST_NAME)
|
||||||
|
jarFile.getInputStream(entry).use { `in` ->
|
||||||
|
return String(`in`.readAllBytes(), StandardCharsets.UTF_8).lines().toList() }
|
||||||
|
}
|
||||||
|
} catch (ignored: FileNotFoundException) {
|
||||||
|
// do nothing if manifest file is not available
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.info(e.toString(), e)
|
||||||
|
}
|
||||||
|
return listOf("${getTitle()} ${getVersion()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.twomartens.wahlrecht.exception
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
|
||||||
|
class HttpStatusException(val status: HttpStatus, message: String, cause: Throwable?):
|
||||||
|
RuntimeException(message, cause)
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package de.twomartens.wahlrecht.interceptor
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.HttpStatusCode
|
||||||
|
import org.springframework.http.client.ClientHttpResponse
|
||||||
|
import org.springframework.util.StreamUtils
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class BufferingClientHttpResponseWrapper(private val response: ClientHttpResponse): ClientHttpResponse {
|
||||||
|
private var body: ByteArray? = null
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getStatusCode(): HttpStatusCode {
|
||||||
|
return response.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Replace calls of getRawStatusCode with getStatusCode", ReplaceWith("getStatusCode()"))
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getRawStatusCode(): Int {
|
||||||
|
return statusCode.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getStatusText(): String {
|
||||||
|
return response.statusText
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHeaders(): HttpHeaders {
|
||||||
|
return response.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getBody(): InputStream {
|
||||||
|
if (body == null) {
|
||||||
|
body = StreamUtils.copyToByteArray(response.body)
|
||||||
|
}
|
||||||
|
return ByteArrayInputStream(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package de.twomartens.wahlrecht.interceptor
|
||||||
|
|
||||||
|
import org.slf4j.MDC
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
abstract class HeaderInterceptor {
|
||||||
|
companion object {
|
||||||
|
const val LOGGER_TRACE_ID = "trace.id"
|
||||||
|
const val LOGGER_REQTYPE_ID = "REQTYPE"
|
||||||
|
|
||||||
|
const val HEADER_FIELD_TRACE_ID = "X-TraceId"
|
||||||
|
const val HEADER_FIELD_B3_TRACE_ID = "x-b3-traceid"
|
||||||
|
const val HEADER_FIELD_TYPE_ID = "x-type"
|
||||||
|
|
||||||
|
const val REQ_TYPE_HEALTHCHECK = "HEALTH_CHECK"
|
||||||
|
const val REQ_TYPE_INTEGRATION_TEST = "INTEGRATION_TEST"
|
||||||
|
const val REQ_TYPE_SERVER_TEST = "SERVER_TEST"
|
||||||
|
const val REQ_TYPE_WARMUP = "WARMUP"
|
||||||
|
|
||||||
|
private fun createNewTraceId(): String {
|
||||||
|
return UUID.randomUUID().toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTraceId(): String {
|
||||||
|
val traceId = MDC.get(LOGGER_TRACE_ID)
|
||||||
|
return if (traceId.isNullOrBlank()) createNewTraceId() else traceId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRequestType(): String? {
|
||||||
|
val type = MDC.get(LOGGER_REQTYPE_ID)
|
||||||
|
return if (type.isNullOrBlank()) null else type
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTraceId(traceId: String): InterceptorCloseables {
|
||||||
|
return InterceptorCloseables(MDC.putCloseable(LOGGER_TRACE_ID, traceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mark(requestType: String?): InterceptorCloseables {
|
||||||
|
return InterceptorCloseables(MDC.putCloseable(LOGGER_REQTYPE_ID, requestType))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun set(traceId: String, requestType: String?): InterceptorCloseables {
|
||||||
|
return if (requestType != null) {
|
||||||
|
InterceptorCloseables(setTraceId(traceId), mark(requestType))
|
||||||
|
} else setTraceId(traceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markAsHealthCheck(): InterceptorCloseables {
|
||||||
|
return InterceptorCloseables(mark(REQ_TYPE_HEALTHCHECK), setTraceId(createNewTraceId()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markAsIntegrationTest(): InterceptorCloseables {
|
||||||
|
return InterceptorCloseables(mark(REQ_TYPE_INTEGRATION_TEST), setTraceId(createNewTraceId()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markAsServerTest(): InterceptorCloseables {
|
||||||
|
return InterceptorCloseables(mark(REQ_TYPE_SERVER_TEST), setTraceId(createNewTraceId()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markAsWarmup(): InterceptorCloseables {
|
||||||
|
return InterceptorCloseables(mark(REQ_TYPE_WARMUP), setTraceId(createNewTraceId()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InterceptorCloseables(vararg closeables: Closeable) : Closeable {
|
||||||
|
private val closeables: Array<out Closeable>
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.closeables = closeables
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
closeables.forEach {
|
||||||
|
try {
|
||||||
|
it.close()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
package de.twomartens.wahlrecht.interceptor
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.http.HttpRequest
|
||||||
|
import org.springframework.http.client.ClientHttpRequestExecution
|
||||||
|
import org.springframework.http.client.ClientHttpRequestInterceptor
|
||||||
|
import org.springframework.http.client.ClientHttpResponse
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor
|
||||||
|
import org.springframework.web.servlet.ModelAndView
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class HeaderInterceptorRest : HeaderInterceptor(), HandlerInterceptor, ClientHttpRequestInterceptor {
|
||||||
|
companion object {
|
||||||
|
val CLASS_NAME: String = HeaderInterceptorRest::class.java.getName()
|
||||||
|
|
||||||
|
fun extractTraceId(request: HttpServletRequest): String {
|
||||||
|
var traceId = request.getHeader(HEADER_FIELD_TRACE_ID)
|
||||||
|
if (traceId.isNullOrBlank()) traceId = request.getHeader(HEADER_FIELD_B3_TRACE_ID)
|
||||||
|
if (traceId.isNullOrBlank()) return UUID.randomUUID().toString()
|
||||||
|
return traceId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractTraceId(request: HttpRequest): String {
|
||||||
|
var traceId = request.headers[HEADER_FIELD_TRACE_ID]?.first()
|
||||||
|
if (traceId.isNullOrBlank()) traceId = request.headers[HEADER_FIELD_B3_TRACE_ID]?.first()
|
||||||
|
if (traceId.isNullOrBlank()) return UUID.randomUUID().toString()
|
||||||
|
return traceId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractRequestType(request: HttpServletRequest): String? {
|
||||||
|
val type = request.getHeader(HEADER_FIELD_TYPE_ID)
|
||||||
|
if (type.isNullOrBlank()) return null
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractRequestType(request: HttpRequest): String? {
|
||||||
|
val type = request.headers[HEADER_FIELD_TYPE_ID]?.first()
|
||||||
|
if (type.isNullOrBlank()) return null
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientHttpRequestInterceptor
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(
|
||||||
|
request: HttpRequest, body: ByteArray,
|
||||||
|
execution: ClientHttpRequestExecution
|
||||||
|
): ClientHttpResponse {
|
||||||
|
request.headers.add(HEADER_FIELD_TRACE_ID, getTraceId())
|
||||||
|
val requestType = getRequestType()
|
||||||
|
if (requestType != null) {
|
||||||
|
request.headers.add(HEADER_FIELD_TYPE_ID, requestType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
execution.execute(request, body)
|
||||||
|
} finally {
|
||||||
|
request.headers.remove(HEADER_FIELD_TRACE_ID)
|
||||||
|
request.headers.remove(HEADER_FIELD_TYPE_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerInterceptor
|
||||||
|
override fun preHandle(
|
||||||
|
request: HttpServletRequest, response: HttpServletResponse,
|
||||||
|
handler: Any
|
||||||
|
): Boolean {
|
||||||
|
val traceId = extractTraceId(request)
|
||||||
|
val requestType = extractRequestType(request)
|
||||||
|
val closeable = set(traceId, requestType)
|
||||||
|
request.setAttribute(CLASS_NAME, closeable)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerInterceptor
|
||||||
|
override fun postHandle(
|
||||||
|
request: HttpServletRequest,
|
||||||
|
response: HttpServletResponse,
|
||||||
|
handler: Any,
|
||||||
|
modelAndView: ModelAndView?
|
||||||
|
) {
|
||||||
|
val obj = request.getAttribute(CLASS_NAME)
|
||||||
|
if (obj != null && obj is InterceptorCloseables) {
|
||||||
|
obj.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+431
@@ -0,0 +1,431 @@
|
|||||||
|
package de.twomartens.wahlrecht.interceptor
|
||||||
|
|
||||||
|
import jakarta.servlet.*
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.apache.logging.log4j.message.StringMapMessage
|
||||||
|
import org.slf4j.Marker
|
||||||
|
import org.slf4j.MarkerFactory
|
||||||
|
import org.springframework.http.*
|
||||||
|
import org.springframework.http.client.ClientHttpRequestExecution
|
||||||
|
import org.springframework.http.client.ClientHttpRequestInterceptor
|
||||||
|
import org.springframework.http.client.ClientHttpResponse
|
||||||
|
import org.springframework.util.StreamUtils
|
||||||
|
import org.springframework.web.util.ContentCachingRequestWrapper
|
||||||
|
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URL
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.*
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
class LoggingInterceptorRest(
|
||||||
|
private val requestLogBehaviour: FieldLogBehaviour,
|
||||||
|
private val responseLogBehaviour: FieldLogBehaviour,
|
||||||
|
val clock: Clock
|
||||||
|
) : Filter, ClientHttpRequestInterceptor {
|
||||||
|
|
||||||
|
constructor(clock: Clock) : this(FieldLogBehaviour.NEVER, FieldLogBehaviour.NEVER, clock)
|
||||||
|
|
||||||
|
@Throws(IOException::class, ServletException::class)
|
||||||
|
override fun doFilter(
|
||||||
|
request: ServletRequest, response: ServletResponse,
|
||||||
|
chain: FilterChain
|
||||||
|
) {
|
||||||
|
val requestTime = ZonedDateTime.now(clock)
|
||||||
|
val httpRequest = request as HttpServletRequest
|
||||||
|
val requestWrapper = ContentCachingRequestWrapper(httpRequest)
|
||||||
|
val responseWrapper = ContentCachingResponseWrapper(
|
||||||
|
(response as HttpServletResponse)
|
||||||
|
)
|
||||||
|
var fullResponseBytes: ByteArray? = null
|
||||||
|
var throwable: Throwable? = null
|
||||||
|
var responseBody: String? = null
|
||||||
|
var httpStatusCode = -1
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
chain.doFilter(
|
||||||
|
if (requestLogBehaviour != FieldLogBehaviour.NEVER) requestWrapper else httpRequest,
|
||||||
|
responseWrapper
|
||||||
|
)
|
||||||
|
if (responseLogBehaviour != FieldLogBehaviour.NEVER) {
|
||||||
|
fullResponseBytes = responseWrapper.contentAsByteArray
|
||||||
|
}
|
||||||
|
httpStatusCode = responseWrapper.status
|
||||||
|
} finally {
|
||||||
|
responseWrapper.copyBodyToResponse()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throwable = e
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
val responseSize = responseWrapper.contentSize
|
||||||
|
val responseHeaders = extractHeaders(
|
||||||
|
responseWrapper.headerNames.iterator()
|
||||||
|
) { responseWrapper.getHeaders(it).iterator() }
|
||||||
|
if (
|
||||||
|
(responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||||
|
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS)
|
||||||
|
&& fullResponseBytes != null
|
||||||
|
) {
|
||||||
|
responseBody = String(
|
||||||
|
fullResponseBytes,
|
||||||
|
determineResponseEncoding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val query = if (httpRequest.queryString != null) "?${httpRequest.queryString}" else ""
|
||||||
|
val requestUrl = URL("${httpRequest.requestURL}$query")
|
||||||
|
val requestHeaders = extractHeaders(httpRequest.headerNames.asIterator()) {
|
||||||
|
httpRequest.getHeaders(it).asIterator()
|
||||||
|
}
|
||||||
|
var requestBody: String? = null
|
||||||
|
var businessType: String? = null
|
||||||
|
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||||
|
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||||
|
) {
|
||||||
|
val fullRequestBytes = requestWrapper.contentAsByteArray
|
||||||
|
requestBody = String(fullRequestBytes, determineRequestEncoding())
|
||||||
|
businessType = determineBusinessType()
|
||||||
|
}
|
||||||
|
log(
|
||||||
|
LogMessage(
|
||||||
|
requestHeaders = requestHeaders,
|
||||||
|
responseHeaders = responseHeaders,
|
||||||
|
url = requestUrl,
|
||||||
|
method = httpRequest.method,
|
||||||
|
requestMimeType = typeToString(request.getContentType()),
|
||||||
|
responseMimeType = typeToString(response.getContentType()),
|
||||||
|
requestBody = requestBody,
|
||||||
|
responseBody = responseBody,
|
||||||
|
requestSize = httpRequest.contentLength,
|
||||||
|
responseSize = responseSize,
|
||||||
|
httpStatus = httpStatusCode,
|
||||||
|
direction = DIRECTION_IN,
|
||||||
|
requestTime = requestTime,
|
||||||
|
responseTime = ZonedDateTime.now(clock),
|
||||||
|
traceId = HeaderInterceptorRest.extractTraceId(httpRequest),
|
||||||
|
requestType = HeaderInterceptorRest.extractRequestType(httpRequest),
|
||||||
|
businessType = businessType,
|
||||||
|
throwable = throwable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: java.lang.RuntimeException) {
|
||||||
|
log.error(e.toString(), e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isError(httpStatusCode: Int): Boolean {
|
||||||
|
return httpStatusCode in 400..599
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(
|
||||||
|
request: HttpRequest,
|
||||||
|
requestBytes: ByteArray, execution: ClientHttpRequestExecution
|
||||||
|
): ClientHttpResponse {
|
||||||
|
val requestTime = ZonedDateTime.now(clock)
|
||||||
|
var responseSize = 0
|
||||||
|
var responseHeaders: Map<String, Collection<String>> = emptyMap()
|
||||||
|
var responseMediaType: MediaType? = null
|
||||||
|
var httpStatusCode = -1
|
||||||
|
var throwable: Throwable? = null
|
||||||
|
var requestBody: String? = null
|
||||||
|
var responseBody: String? = null
|
||||||
|
var businessType: String? = null
|
||||||
|
return try {
|
||||||
|
val result = BufferingClientHttpResponseWrapper(
|
||||||
|
execution.execute(request, requestBytes)
|
||||||
|
)
|
||||||
|
val responseBytes = StreamUtils.copyToByteArray(result.getBody())
|
||||||
|
responseSize = responseBytes.size
|
||||||
|
responseHeaders = extractHeaders(result.headers)
|
||||||
|
responseMediaType = result.headers.getContentType()
|
||||||
|
httpStatusCode = result.statusCode.value()
|
||||||
|
if (responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||||
|
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||||
|
) {
|
||||||
|
responseBody = String(
|
||||||
|
responseBytes,
|
||||||
|
determineRequestEncoding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throwable = e
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
val url = request.uri.toURL()
|
||||||
|
val requestHeaders: Map<String, Collection<String>> = extractHeaders(request.headers)
|
||||||
|
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|
||||||
|
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS
|
||||||
|
) {
|
||||||
|
requestBody = String(
|
||||||
|
requestBytes,
|
||||||
|
determineRequestEncoding()
|
||||||
|
)
|
||||||
|
businessType = determineBusinessType()
|
||||||
|
}
|
||||||
|
log(
|
||||||
|
LogMessage(
|
||||||
|
requestHeaders = requestHeaders,
|
||||||
|
responseHeaders = responseHeaders,
|
||||||
|
url = url,
|
||||||
|
method = request.method.name(),
|
||||||
|
requestMimeType = typeToString(request.headers.getContentType()),
|
||||||
|
requestBody = requestBody,
|
||||||
|
responseBody = responseBody,
|
||||||
|
responseMimeType = typeToString(responseMediaType!!),
|
||||||
|
requestSize = requestBytes.size,
|
||||||
|
responseSize = responseSize,
|
||||||
|
httpStatus = httpStatusCode,
|
||||||
|
direction = DIRECTION_OUT,
|
||||||
|
requestTime = requestTime,
|
||||||
|
responseTime = ZonedDateTime.now(clock),
|
||||||
|
businessType = businessType,
|
||||||
|
throwable = throwable,
|
||||||
|
traceId = HeaderInterceptorRest.extractTraceId(request),
|
||||||
|
requestType = HeaderInterceptorRest.extractRequestType(request),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: java.lang.RuntimeException) {
|
||||||
|
log.error(e.toString(), e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun log(logMessage: LogMessage) {
|
||||||
|
val stringMapMessage = StringMapMessage()
|
||||||
|
addLogString(
|
||||||
|
stringMapMessage, PARAM_REQUEST_HEADERS,
|
||||||
|
toHeaderString(logMessage.requestHeaders)
|
||||||
|
)
|
||||||
|
addLogString(
|
||||||
|
stringMapMessage, PARAM_RESPONSE_HEADERS,
|
||||||
|
toHeaderString(logMessage.responseHeaders)
|
||||||
|
)
|
||||||
|
addLogString(stringMapMessage, PARAM_URL_FULL, logMessage.url.toString())
|
||||||
|
addLogString(stringMapMessage, PARAM_URL_DOMAIN, logMessage.url.host)
|
||||||
|
addLogString(
|
||||||
|
stringMapMessage, PARAM_URL_EXTENSION,
|
||||||
|
extractExtension(logMessage.url.path)
|
||||||
|
)
|
||||||
|
addLogString(stringMapMessage, PARAM_URL_PATH, logMessage.url.path)
|
||||||
|
addLogString(stringMapMessage, PARAM_URL_PORT, logMessage.url.port.toString())
|
||||||
|
addLogString(stringMapMessage, PARAM_URL_SCHEME, logMessage.url.protocol)
|
||||||
|
addLogString(stringMapMessage, PARAM_URL_QUERY, logMessage.url.query)
|
||||||
|
addLogString(stringMapMessage, PARAM_REQUEST_METHOD, logMessage.method)
|
||||||
|
addLogString(
|
||||||
|
stringMapMessage, PARAM_REQUEST_REFERER,
|
||||||
|
getHeader(logMessage.requestHeaders, HttpHeaders.REFERER)
|
||||||
|
)
|
||||||
|
addLogString(stringMapMessage, PARAM_REQUEST_MIMETYPE, logMessage.requestMimeType)
|
||||||
|
addLogString(stringMapMessage, PARAM_RESPONSE_MIMETYPE, logMessage.responseMimeType)
|
||||||
|
addLogString(stringMapMessage, PARAM_REQUEST_BYTES, logMessage.requestSize.toString())
|
||||||
|
addLogString(stringMapMessage, PARAM_RESPONSE_BYTES, logMessage.responseSize.toString())
|
||||||
|
addLogString(stringMapMessage, PARAM_RESPONSE_STATUS, logMessage.httpStatus.toString())
|
||||||
|
addLogString(stringMapMessage, PARAM_DIRECTION, logMessage.direction)
|
||||||
|
addLogString(stringMapMessage, PARAM_PROTOCOL, PROTOCOL_NAME)
|
||||||
|
addLogString(
|
||||||
|
stringMapMessage, PARAM_REQUEST_TIME,
|
||||||
|
logMessage.requestTime.format(DATE_TIME_FORMATTER)
|
||||||
|
)
|
||||||
|
addLogString(
|
||||||
|
stringMapMessage, PARAM_RESPONSE_TIME,
|
||||||
|
logMessage.responseTime.format(DATE_TIME_FORMATTER)
|
||||||
|
)
|
||||||
|
addLogString(
|
||||||
|
stringMapMessage,
|
||||||
|
PARAM_DURATION,
|
||||||
|
getDurationBetweenRequestAndResponseTime(logMessage).toNanos().toString()
|
||||||
|
)
|
||||||
|
addLogString(
|
||||||
|
stringMapMessage, PARAM_USER_AGENT,
|
||||||
|
getHeader(logMessage.requestHeaders, HttpHeaders.USER_AGENT)
|
||||||
|
)
|
||||||
|
addLogString(stringMapMessage, HeaderInterceptor.LOGGER_TRACE_ID, logMessage.traceId)
|
||||||
|
addLogString(stringMapMessage, HeaderInterceptor.LOGGER_REQTYPE_ID, logMessage.requestType)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDurationBetweenRequestAndResponseTime(logMessage: LogMessage): Duration {
|
||||||
|
return Duration.between(logMessage.requestTime, logMessage.responseTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHeader(headers: Map<String, Collection<String?>>, headerKey: String): String? {
|
||||||
|
return headers.entries.asSequence()
|
||||||
|
.filter { it.key.equals(headerKey, ignoreCase = true) }
|
||||||
|
.flatMap { it.value.asSequence() }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addLogString(stringMapMessage: StringMapMessage, key: String, value: String?) {
|
||||||
|
if (!value.isNullOrBlank()) {
|
||||||
|
stringMapMessage.with(key, value.trim { it <= ' ' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* usually returns null, but can be overridden to implement more complex logic
|
||||||
|
*/
|
||||||
|
private fun determineBusinessType(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* usually returns UTF-8, but can be overridden to implement more complex logic
|
||||||
|
*/
|
||||||
|
private fun determineRequestEncoding(): Charset {
|
||||||
|
return StandardCharsets.UTF_8
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* usually returns UTF-8, but can be overridden to implement more complex logic
|
||||||
|
*/
|
||||||
|
private fun determineResponseEncoding(): Charset {
|
||||||
|
return StandardCharsets.UTF_8
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
private const val MAX_LOG_SIZE = 20480 // 20 KB - logging could fail with bigger logmessages
|
||||||
|
|
||||||
|
val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
|
||||||
|
val MARKER: Marker = MarkerFactory.getMarker("communication")
|
||||||
|
|
||||||
|
const val DIRECTION_IN = "inbound"
|
||||||
|
const val DIRECTION_OUT = "outbound"
|
||||||
|
const val PROTOCOL_NAME = "http"
|
||||||
|
|
||||||
|
const val PARAM_URL_FULL = "url.full"
|
||||||
|
const val PARAM_URL_DOMAIN = "url.domain"
|
||||||
|
const val PARAM_URL_EXTENSION = "url.extension"
|
||||||
|
const val PARAM_URL_PATH = "url.path"
|
||||||
|
const val PARAM_URL_PORT = "url.port"
|
||||||
|
const val PARAM_URL_SCHEME = "url.scheme"
|
||||||
|
const val PARAM_URL_QUERY = "url.query"
|
||||||
|
const val PARAM_BUSINESS_TYPE = "http.request.type"
|
||||||
|
const val PARAM_REQUEST_BODY = "http.request.body.content"
|
||||||
|
const val PARAM_RESPONSE_BODY = "http.response.body.content"
|
||||||
|
const val PARAM_RESPONSE_STATUS = "http.response.status_code"
|
||||||
|
const val PARAM_REQUEST_HEADERS = "http.request.headers"
|
||||||
|
const val PARAM_RESPONSE_HEADERS = "http.response.headers"
|
||||||
|
const val PARAM_REQUEST_BYTES = "http.request.body.bytes"
|
||||||
|
const val PARAM_RESPONSE_BYTES = "http.response.body.bytes"
|
||||||
|
const val PARAM_REQUEST_MIMETYPE = "http.request.mime_type"
|
||||||
|
const val PARAM_RESPONSE_MIMETYPE = "http.response.mime_type"
|
||||||
|
const val PARAM_REQUEST_METHOD = "http.request.method"
|
||||||
|
const val PARAM_REQUEST_REFERER = "http.request.referrer"
|
||||||
|
const val PARAM_REQUEST_TIME = "event.start"
|
||||||
|
const val PARAM_RESPONSE_TIME = "event.end"
|
||||||
|
const val PARAM_DURATION = "event.duration"
|
||||||
|
const val PARAM_USER_AGENT = "user_agent.original"
|
||||||
|
const val PARAM_DIRECTION = "network.direction"
|
||||||
|
const val PARAM_PROTOCOL = "network.protocol"
|
||||||
|
|
||||||
|
private fun extractHeaders(
|
||||||
|
headerNames: Iterator<String>,
|
||||||
|
headerValuesSupplier: Function<String, Iterator<String>>
|
||||||
|
): Map<String, MutableCollection<String>> {
|
||||||
|
val requestHeaders: MutableMap<String, MutableCollection<String>> = mutableMapOf()
|
||||||
|
while (headerNames.hasNext()) {
|
||||||
|
val name = headerNames.next()
|
||||||
|
val values = requestHeaders.computeIfAbsent(name) { mutableSetOf() }
|
||||||
|
val headerValues = headerValuesSupplier.apply(name)
|
||||||
|
while (headerValues.hasNext()) {
|
||||||
|
values.add(headerValues.next())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractHeaders(headers: HttpHeaders): Map<String, Collection<String>> {
|
||||||
|
val result: MutableMap<String, Collection<String>> = mutableMapOf()
|
||||||
|
for ((key, value) in headers) {
|
||||||
|
result[key] = value.toList()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toHeaderString(headerMap: Map<String, Collection<String>>): String {
|
||||||
|
return headerMap.entries.asSequence()
|
||||||
|
.flatMap { entry ->
|
||||||
|
val key = entry.key
|
||||||
|
entry.value.asSequence().map { Pair(key, it) }
|
||||||
|
}
|
||||||
|
.map { "${it.first}=${it.second}" }
|
||||||
|
.joinToString(separator = ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun typeToString(contentType: String?): String? {
|
||||||
|
return try {
|
||||||
|
if (contentType == null) return null
|
||||||
|
val mediaType = MediaType.parseMediaType(contentType)
|
||||||
|
return typeToString(mediaType)
|
||||||
|
} catch (e: InvalidMediaTypeException) {
|
||||||
|
log.info(e.toString(), e)
|
||||||
|
e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun typeToString(mediaType: MediaType?): String? {
|
||||||
|
return try {
|
||||||
|
if (mediaType == null) return null
|
||||||
|
"${mediaType.type}/${mediaType.subtype}"
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
log.info(e.toString(), e)
|
||||||
|
e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractExtension(fileName: String?): String? {
|
||||||
|
if (fileName == null || !fileName.contains(".")) return null
|
||||||
|
return fileName.substring(fileName.lastIndexOf('.') + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cutToMaxLength(string: String?): String? {
|
||||||
|
return if (string != null && string.length > MAX_LOG_SIZE) {
|
||||||
|
string.substring(0, MAX_LOG_SIZE)
|
||||||
|
} else string
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LogMessage(
|
||||||
|
val requestHeaders: Map<String, Collection<String>>,
|
||||||
|
val responseHeaders: Map<String, Collection<String>>,
|
||||||
|
val url: URL,
|
||||||
|
val method: String,
|
||||||
|
val requestMimeType: String?,
|
||||||
|
val responseMimeType: String?,
|
||||||
|
val requestBody: String?,
|
||||||
|
val responseBody: String?,
|
||||||
|
val requestSize: Int,
|
||||||
|
val responseSize: Int,
|
||||||
|
val httpStatus: Int,
|
||||||
|
val direction: String,
|
||||||
|
val requestTime: ZonedDateTime,
|
||||||
|
val responseTime: ZonedDateTime,
|
||||||
|
val traceId: String?,
|
||||||
|
val requestType: String?,
|
||||||
|
val businessType: String?,
|
||||||
|
val throwable: Throwable?
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class FieldLogBehaviour {
|
||||||
|
NEVER,
|
||||||
|
ONLY_ON_ERROR,
|
||||||
|
ALWAYS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.db.Candidate
|
||||||
|
import org.mapstruct.*
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface CandidateMapper {
|
||||||
|
fun mapToExternal(candidate: Candidate): de.twomartens.wahlrecht.model.dto.v1.Candidate
|
||||||
|
|
||||||
|
fun mapToInternal(
|
||||||
|
candidate: de.twomartens.wahlrecht.model.dto.v1.Candidate
|
||||||
|
): de.twomartens.wahlrecht.model.internal.Candidate
|
||||||
|
|
||||||
|
fun mapToInternal(candidate: Candidate): de.twomartens.wahlrecht.model.internal.Candidate
|
||||||
|
|
||||||
|
@Mapping(target = "id", ignore = true)
|
||||||
|
@Mapping(target = "created", ignore = true)
|
||||||
|
@Mapping(target = "lastModified", ignore = true)
|
||||||
|
fun mapToDB(candidate: de.twomartens.wahlrecht.model.dto.v1.Candidate): Candidate
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.db.Constituency
|
||||||
|
import org.mapstruct.*
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface ConstituencyMapper {
|
||||||
|
fun mapToExternal(constituency: Constituency): de.twomartens.wahlrecht.model.dto.v1.Constituency
|
||||||
|
|
||||||
|
fun mapToInternal(constituency: de.twomartens.wahlrecht.model.dto.v1.Constituency): de.twomartens.wahlrecht.model.internal.Constituency
|
||||||
|
|
||||||
|
fun mapToInternal(constituency: Constituency): de.twomartens.wahlrecht.model.internal.Constituency
|
||||||
|
|
||||||
|
@Mapping(target = "id", ignore = true)
|
||||||
|
@Mapping(target = "created", ignore = true)
|
||||||
|
@Mapping(target = "lastModified", ignore = true)
|
||||||
|
fun mapToDB(constituency: de.twomartens.wahlrecht.model.dto.v1.Constituency): Constituency
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.internal.ElectedCandidate
|
||||||
|
import org.mapstruct.CollectionMappingStrategy
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.NullValueCheckStrategy
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface ElectedCandidateMapper {
|
||||||
|
fun mapToExternal(candidate: ElectedCandidate): de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate
|
||||||
|
|
||||||
|
fun mapToInternal(candidate: de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate): ElectedCandidate
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.internal.ElectedCandidate
|
||||||
|
import de.twomartens.wahlrecht.model.internal.ElectedCandidates
|
||||||
|
import de.twomartens.wahlrecht.model.internal.VotingResult
|
||||||
|
import org.mapstruct.CollectionMappingStrategy
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.NullValueCheckStrategy
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
import org.mapstruct.factory.Mappers
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface ElectedCandidatesMapper {
|
||||||
|
companion object {
|
||||||
|
val ELECTED_RESULT_MAPPER: ElectedResultMapper = Mappers.getMapper(ElectedResultMapper::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun mapToExternal(candidate: ElectedCandidates): de.twomartens.wahlrecht.model.dto.v1.ElectedCandidates
|
||||||
|
|
||||||
|
fun mapToExternal(value: Map<VotingResult, Collection<ElectedCandidate>>):
|
||||||
|
Map<String, Collection<de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate>> {
|
||||||
|
return ELECTED_RESULT_MAPPER.mapToExternal(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.internal.Elected
|
||||||
|
import org.mapstruct.CollectionMappingStrategy
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.NullValueCheckStrategy
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface ElectedMapper {
|
||||||
|
fun mapToExternal(elected: Elected): de.twomartens.wahlrecht.model.dto.v1.Elected
|
||||||
|
|
||||||
|
fun mapToInternal(elected: de.twomartens.wahlrecht.model.dto.v1.Elected): Elected
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.internal.ElectedCandidate
|
||||||
|
import de.twomartens.wahlrecht.model.internal.ElectedResult
|
||||||
|
import de.twomartens.wahlrecht.model.internal.NominationId
|
||||||
|
import de.twomartens.wahlrecht.model.internal.VotingResult
|
||||||
|
import org.mapstruct.CollectionMappingStrategy
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.NullValueCheckStrategy
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface ElectedResultMapper {
|
||||||
|
fun mapToExternal(result: ElectedResult): de.twomartens.wahlrecht.model.dto.v1.ElectedResult
|
||||||
|
|
||||||
|
fun mapToExternal(value: Map<VotingResult, Collection<ElectedCandidate>>):
|
||||||
|
Map<String, Collection<de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate>> {
|
||||||
|
return value.entries.asSequence()
|
||||||
|
.map { Pair(it.key.nominationId.partyAbbreviation, mapToExternal(it.value)) }
|
||||||
|
.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToExternal(value: Collection<ElectedCandidate>):
|
||||||
|
Collection<de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate>
|
||||||
|
|
||||||
|
fun map(value: Map<VotingResult, Collection<ElectedCandidate>>):
|
||||||
|
Map<NominationId, Collection<ElectedCandidate>> {
|
||||||
|
return value.entries.asSequence()
|
||||||
|
.map { Pair(it.key.nominationId, it.value) }
|
||||||
|
.toMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.db.Election
|
||||||
|
import org.mapstruct.*
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface ElectionMapper {
|
||||||
|
fun mapToExternal(election: Election): de.twomartens.wahlrecht.model.dto.v1.Election
|
||||||
|
|
||||||
|
fun mapToInternal(
|
||||||
|
election: de.twomartens.wahlrecht.model.dto.v1.Election
|
||||||
|
): de.twomartens.wahlrecht.model.internal.Election
|
||||||
|
|
||||||
|
fun mapToInternal(election: Election): de.twomartens.wahlrecht.model.internal.Election
|
||||||
|
|
||||||
|
@Mapping(target = "id", ignore = true)
|
||||||
|
@Mapping(target = "created", ignore = true)
|
||||||
|
@Mapping(target = "lastModified", ignore = true)
|
||||||
|
fun mapToDB(election: de.twomartens.wahlrecht.model.dto.v1.Election): Election
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.db.ElectionResult
|
||||||
|
import de.twomartens.wahlrecht.model.db.VotingResult
|
||||||
|
import org.mapstruct.*
|
||||||
|
import org.mapstruct.factory.Mappers
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface ElectionResultMapper {
|
||||||
|
companion object {
|
||||||
|
val VOTING_RESULT_MAPPER: VotingResultMapper = Mappers.getMapper(VotingResultMapper::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToExternal(result: ElectionResult): de.twomartens.wahlrecht.model.dto.v1.ElectionResult
|
||||||
|
|
||||||
|
fun mapToExternal(results: Collection<VotingResult>): Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>
|
||||||
|
|
||||||
|
fun mapToExternal(results: Map<Int, Collection<VotingResult>>):
|
||||||
|
Map<Int, Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>>
|
||||||
|
|
||||||
|
@Mapping(target = "id", ignore = true)
|
||||||
|
@Mapping(target = "created", ignore = true)
|
||||||
|
@Mapping(target = "lastModified", ignore = true)
|
||||||
|
fun mapToDB(result: de.twomartens.wahlrecht.model.dto.v1.ElectionResult): ElectionResult
|
||||||
|
|
||||||
|
fun mapToDB(results: Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>): Collection<VotingResult>
|
||||||
|
|
||||||
|
fun mapToDB(results: Map<Int, Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>>):
|
||||||
|
Map<Int, Collection<VotingResult>>
|
||||||
|
|
||||||
|
fun mapToInternal(result: de.twomartens.wahlrecht.model.dto.v1.ElectionResult):
|
||||||
|
de.twomartens.wahlrecht.model.internal.ElectionResult
|
||||||
|
|
||||||
|
fun mapToInternal(result: de.twomartens.wahlrecht.model.dto.v1.VotingResult):
|
||||||
|
de.twomartens.wahlrecht.model.internal.VotingResult {
|
||||||
|
return VOTING_RESULT_MAPPER.mapToInternal(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToInternal(results: Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>):
|
||||||
|
Collection<de.twomartens.wahlrecht.model.internal.VotingResult>
|
||||||
|
|
||||||
|
fun mapToInternal(results: Map<Int, Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult>>):
|
||||||
|
Map<Int, Collection<de.twomartens.wahlrecht.model.internal.VotingResult>>
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.internal.NominationId
|
||||||
|
import org.mapstruct.CollectionMappingStrategy
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.NullValueCheckStrategy
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface NominationIdMapper {
|
||||||
|
fun mapToExternal(id: NominationId): de.twomartens.wahlrecht.model.dto.v1.NominationId
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.db.Candidate
|
||||||
|
import de.twomartens.wahlrecht.model.db.Nomination
|
||||||
|
import de.twomartens.wahlrecht.model.internal.NominationId
|
||||||
|
import org.mapstruct.*
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface NominationMapper {
|
||||||
|
fun mapToExternal(nomination: Nomination): de.twomartens.wahlrecht.model.dto.v1.Nomination
|
||||||
|
|
||||||
|
fun mapToInternal(
|
||||||
|
nomination: de.twomartens.wahlrecht.model.dto.v1.Nomination
|
||||||
|
): de.twomartens.wahlrecht.model.internal.Nomination {
|
||||||
|
val result = de.twomartens.wahlrecht.model.internal.Nomination(
|
||||||
|
id = NominationId(
|
||||||
|
electionName = nomination.electionName,
|
||||||
|
partyAbbreviation = nomination.partyAbbreviation,
|
||||||
|
name = nomination.name
|
||||||
|
),
|
||||||
|
supportVotesOnNomination = nomination.supportVotesOnNomination
|
||||||
|
)
|
||||||
|
result.addCandidates(mapFromDto(nomination.candidates))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToInternal(nomination: Nomination): de.twomartens.wahlrecht.model.internal.Nomination {
|
||||||
|
val result = de.twomartens.wahlrecht.model.internal.Nomination(
|
||||||
|
id = NominationId(
|
||||||
|
electionName = nomination.electionName,
|
||||||
|
partyAbbreviation = nomination.partyAbbreviation,
|
||||||
|
name = nomination.name
|
||||||
|
),
|
||||||
|
supportVotesOnNomination = nomination.supportVotesOnNomination
|
||||||
|
)
|
||||||
|
result.addCandidates(mapFromDb(nomination.candidates))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mapping(target = "id", ignore = true)
|
||||||
|
@Mapping(target = "created", ignore = true)
|
||||||
|
@Mapping(target = "lastModified", ignore = true)
|
||||||
|
fun mapToDB(nomination: de.twomartens.wahlrecht.model.dto.v1.Nomination): Nomination
|
||||||
|
|
||||||
|
fun mapFromDb(candidates: Collection<Candidate>): Collection<de.twomartens.wahlrecht.model.internal.Candidate>
|
||||||
|
|
||||||
|
fun mapFromDto(candidates: Collection<de.twomartens.wahlrecht.model.dto.v1.Candidate>): Collection<de.twomartens.wahlrecht.model.internal.Candidate>
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.db.PartyInElection
|
||||||
|
import org.mapstruct.*
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface PartyInElectionMapper {
|
||||||
|
fun mapToExternal(election: PartyInElection): de.twomartens.wahlrecht.model.dto.v1.PartyInElection
|
||||||
|
|
||||||
|
@Mapping(target = "id", ignore = true)
|
||||||
|
@Mapping(target = "created", ignore = true)
|
||||||
|
@Mapping(target = "lastModified", ignore = true)
|
||||||
|
fun mapToDB(election: de.twomartens.wahlrecht.model.dto.v1.PartyInElection): PartyInElection
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.internal.SeatResult
|
||||||
|
import org.mapstruct.CollectionMappingStrategy
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.NullValueCheckStrategy
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface SeatResultMapper {
|
||||||
|
fun mapToExternal(result: SeatResult): de.twomartens.wahlrecht.model.dto.v1.SeatResult
|
||||||
|
|
||||||
|
fun mapToInternal(result: de.twomartens.wahlrecht.model.dto.v1.SeatResult): SeatResult
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.internal.NominationId
|
||||||
|
import de.twomartens.wahlrecht.model.internal.VotingResult
|
||||||
|
import org.mapstruct.CollectionMappingStrategy
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.NullValueCheckStrategy
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
import org.springframework.lang.NonNull
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface VotingResultMapper {
|
||||||
|
fun mapToExternal(result: VotingResult): de.twomartens.wahlrecht.model.dto.v1.VotingResult {
|
||||||
|
return de.twomartens.wahlrecht.model.dto.v1.VotingResult(
|
||||||
|
electionName = result.nominationId.electionName,
|
||||||
|
partyAbbreviation = result.nominationId.partyAbbreviation,
|
||||||
|
nominationName = result.nominationId.name,
|
||||||
|
votesOnNomination = result.votesOnNomination,
|
||||||
|
votesThroughHealing = result.votesThroughHealing,
|
||||||
|
votesPerPosition = result.votesPerPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToInternal(@NonNull result: de.twomartens.wahlrecht.model.dto.v1.VotingResult): VotingResult {
|
||||||
|
return VotingResult(
|
||||||
|
nominationId = NominationId(
|
||||||
|
electionName = result.electionName,
|
||||||
|
partyAbbreviation = result.partyAbbreviation,
|
||||||
|
name = result.nominationName
|
||||||
|
),
|
||||||
|
votesOnNomination = result.votesOnNomination,
|
||||||
|
votesThroughHealing = result.votesThroughHealing,
|
||||||
|
votesPerPosition = result.votesPerPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package de.twomartens.wahlrecht.mapper.v1
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.internal.VotingThreshold
|
||||||
|
import org.mapstruct.CollectionMappingStrategy
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.NullValueCheckStrategy
|
||||||
|
import org.mapstruct.ReportingPolicy
|
||||||
|
|
||||||
|
@Mapper(
|
||||||
|
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
|
||||||
|
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
interface VotingThresholdMapper {
|
||||||
|
fun mapToExternal(threshold: VotingThreshold): de.twomartens.wahlrecht.model.dto.v1.VotingThreshold
|
||||||
|
|
||||||
|
fun mapToInternal(threshold: de.twomartens.wahlrecht.model.dto.v1.VotingThreshold): VotingThreshold
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.db
|
||||||
|
|
||||||
|
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
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@CompoundIndex(def = "{'name': 1, 'partyAbbreviation': 1, 'electionName': 1}")
|
||||||
|
class Nomination {
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
@Id
|
||||||
|
var id: ObjectId? = null
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
@CreatedDate
|
||||||
|
var created: Instant? = null
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
@LastModifiedDate
|
||||||
|
var lastModified: Instant? = null
|
||||||
|
|
||||||
|
var name: String = ""
|
||||||
|
var partyAbbreviation: String = ""
|
||||||
|
var electionName: String = ""
|
||||||
|
var candidates: Collection<Candidate> = emptyList()
|
||||||
|
var supportVotesOnNomination = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto
|
||||||
|
|
||||||
|
data class ErrorMessage(val message: String)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class Candidate(val name: String, val profession: String)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class Constituency(val electionName: String, val number: Int, val name: String, val numberOfSeats: Int) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
enum class Elected {
|
||||||
|
NOT_ELECTED,
|
||||||
|
CONSTITUENCY,
|
||||||
|
OVERALL_NOMINATION_ORDER,
|
||||||
|
OVERALL_VOTE_ORDER
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class ElectedCandidate(val candidate: Candidate, val elected: Elected)
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class ElectedCandidates(val overallResult: ElectedResult,
|
||||||
|
val constituencyResults: Map<Int, ElectedResult>,
|
||||||
|
val electionNumbersForSeatAllocation: List<Double>)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class ElectedResult(val electedCandidates: Map<String, Collection<ElectedCandidate>>,
|
||||||
|
val usedElectionNumbers: List<Double>)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
data class Election(val name: String, val day: LocalDate, val votingThreshold: VotingThreshold,
|
||||||
|
val totalNumberOfSeats: Int, val constituencies: Collection<Constituency>)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class ElectionResult(val electionName: String, val overallResults: Collection<VotingResult>,
|
||||||
|
val constituencyResults: Map<Int, Collection<VotingResult>>)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class Nomination(var electionName: String,
|
||||||
|
var partyAbbreviation: String,
|
||||||
|
var name: String,
|
||||||
|
var supportVotesOnNomination: Boolean,
|
||||||
|
var candidates: List<Candidate>)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class NominationId(val electionName: String, val partyAbbreviation: String, val name: String)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class PartyInElection(var electionName: String?, var abbreviation: String?, var name: String?,
|
||||||
|
var nomination: Nomination?,
|
||||||
|
var constituencyNominations: Map<Int, Nomination>? = HashMap())
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class SeatResult(val seatsPerResult: Map<VotingResult, Int>,
|
||||||
|
val usedElectionNumbers: List<Double>)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
data class VotingResult(val electionName: String, val partyAbbreviation: String,
|
||||||
|
val nominationName: String,
|
||||||
|
val votesOnNomination: Int, val votesThroughHealing: Int,
|
||||||
|
val votesPerPosition: Map<Int, Int>)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.twomartens.wahlrecht.model.dto.v1
|
||||||
|
|
||||||
|
enum class VotingThreshold {
|
||||||
|
NONE,
|
||||||
|
THREE,
|
||||||
|
FIVE;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user