Moved plenty files over to Kotlin
This commit is contained in:
@@ -1,13 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MainApplication" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="MAIN_CLASS_NAME" value="de.twomartens.wahlrecht.MainApplication" />
|
||||
<configuration default="false" name="MainApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<module name="wahlrecht.server.main" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="de.twomartens.wahlrecht.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="de.twomartens.wahlrecht.MainApplication" />
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="true" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
|
||||
@@ -8,4 +8,11 @@ val libs = the<LibrariesForLibs>()
|
||||
|
||||
dependencies {
|
||||
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"
|
||||
mockito = "5.3.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-lombok = "8.0.1"
|
||||
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-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
|
||||
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-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" }
|
||||
@@ -128,3 +131,6 @@ test = [
|
||||
"mockito-inline",
|
||||
"mockito-junit",
|
||||
]
|
||||
|
||||
[plugins]
|
||||
kotlin-lombok = { id = "org.jetbrains.kotlin.plugin.lombok", version.ref = "kotlin-lombok" }
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
plugins {
|
||||
id("twomartens.spring-boot-cloud")
|
||||
id("twomartens.kotlin")
|
||||
kotlin("kapt")
|
||||
alias(libs.plugins.kotlin.lombok)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.mapstruct.base)
|
||||
implementation(libs.bundles.spring.boot.security)
|
||||
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;
|
||||
|
||||
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
@@ -13,6 +10,10 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A Health check which checks if the rest services are working.
|
||||
* <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;
|
||||
}
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
package de.twomartens.wahlrecht.repository;
|
||||
|
||||
import de.twomartens.wahlrecht.model.db.Nomination;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface NominationRepository extends MongoRepository<Nomination, String> {
|
||||
|
||||
Optional<Nomination> findByNameAndElectionNameAndPartyAbbreviation(String name,
|
||||
|
||||
@@ -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