Moved plenty files over to Kotlin

This commit is contained in:
2023-07-30 15:16:24 +02:00
parent 5f070ad649
commit 50b2cc4f63
117 changed files with 2423 additions and 2582 deletions
@@ -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());
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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
}
}
}
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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()
);
}
}
@@ -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) {
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
@@ -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>
@@ -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;
}
@@ -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;
}
@@ -1,11 +1,12 @@
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,
String electionName, String partyAbbreviation);
String electionName, String partyAbbreviation);
}
@@ -1,323 +0,0 @@
package de.twomartens.wahlrecht.service;
import de.twomartens.wahlrecht.model.internal.Candidate;
import de.twomartens.wahlrecht.model.internal.Constituency;
import de.twomartens.wahlrecht.model.internal.Elected;
import de.twomartens.wahlrecht.model.internal.ElectedCandidate;
import de.twomartens.wahlrecht.model.internal.ElectedCandidates;
import de.twomartens.wahlrecht.model.internal.ElectedResult;
import de.twomartens.wahlrecht.model.internal.Election;
import de.twomartens.wahlrecht.model.internal.ElectionResult;
import de.twomartens.wahlrecht.model.internal.Nomination;
import de.twomartens.wahlrecht.model.internal.NominationId;
import de.twomartens.wahlrecht.model.internal.SeatResult;
import de.twomartens.wahlrecht.model.internal.VotingResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
@RequiredArgsConstructor
@Service
@Slf4j
public class CalculationService {
private final NominationService nominationService;
private final ElectionService electionService;
private final LinkedList<Double> electionNumberHistory = new LinkedList<>();
public ElectedCandidates determineElectedCandidates(@NonNull ElectionResult electionResult) {
log.info("Calculate election result for election {}", electionResult.electionName());
StopWatch stopWatch = new StopWatch();
stopWatch.start("determineElectedCandidates");
Election election = electionService.getElectionInternal(electionResult.electionName());
SeatResult seatResult = calculateOverallSeatDistribution(election,
electionResult.overallResults());
Map<Integer, ElectedResult> constituencyResults = new HashMap<>();
for (Constituency constituency : election.constituencies()) {
ElectedResult electedResult = calculateConstituency(constituency,
electionResult.constituencyResults().get(constituency.number()));
constituencyResults.put(constituency.number(), electedResult);
}
Map<String, Collection<ElectedCandidate>> electedCandidatesMap = constituencyResults
.entrySet().stream()
.flatMap(entry -> entry.getValue().electedCandidates().entrySet().stream())
.collect(Collectors.groupingBy(entry -> entry.getKey().getNominationId().partyAbbreviation(),
Collectors.flatMapping(entry -> entry.getValue().stream(),
Collectors.toCollection(ArrayList::new))));
Map<VotingResult, Integer> remainingSeatsPerParty = seatResult.seatsPerResult()
.entrySet().stream()
.map(entry -> Map.entry(entry.getKey(),
entry.getValue() - electedCandidatesMap.getOrDefault(
entry.getKey().getNominationId().partyAbbreviation(),
Collections.emptyList()).size()))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
ElectedResult overallElectedCandidates = calculateElectedOverallCandidates(
remainingSeatsPerParty,
electedCandidatesMap
);
stopWatch.stop();
log.debug("determineCandidates took {} seconds", stopWatch.getTotalTimeSeconds());
return ElectedCandidates.builder()
.overallResult(overallElectedCandidates)
.constituencyResults(constituencyResults)
.electionNumbersForSeatAllocation(seatResult.usedElectionNumbers())
.build();
}
public ElectedResult calculateConstituency(@NonNull Constituency constituency,
@NonNull Collection<VotingResult> votingResults) {
electionNumberHistory.clear();
log.info("Calculate constituency {}", constituency.name());
int totalVotes = votingResults.stream()
.map(VotingResult::getTotalVotes)
.reduce(0, Integer::sum);
int numberOfSeats = constituency.numberOfSeats();
double initialElectionNumber = totalVotes / (double) numberOfSeats;
Map<VotingResult, Integer> assignedSeatsPerResult = calculateAssignedSeatsPerResult(
votingResults, numberOfSeats, initialElectionNumber);
return ElectedResult.builder()
.electedCandidates(findElectedCandidates(assignedSeatsPerResult, Collections.emptyMap()))
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.build();
}
public SeatResult calculateOverallSeatDistribution(@NonNull Election election,
@NonNull Collection<VotingResult> votingResults) {
log.info("Calculate overall seat distribution for election {}", election.name());
electionNumberHistory.clear();
int totalVotes = votingResults.stream()
.map(VotingResult::getTotalVotes)
.reduce(0, Integer::sum);
List<VotingResult> validVotingResults = new ArrayList<>();
int totalIgnoredVotes = 0;
for (VotingResult votingResult : votingResults) {
if (passesVotingThreshold(election, totalVotes, votingResult)) {
log.info("Party passes voting threshold: {}",
votingResult.getNominationId().partyAbbreviation());
validVotingResults.add(votingResult);
} else {
log.info("Party fails voting threshold: {}",
votingResult.getNominationId().partyAbbreviation());
totalIgnoredVotes += votingResult.getTotalVotes();
}
}
totalVotes -= totalIgnoredVotes;
int numberOfSeats = election.totalNumberOfSeats();
double initialElectionNumber = totalVotes / (double) numberOfSeats;
Map<VotingResult, Integer> assignedSeatsPerResult = calculateAssignedSeatsPerResult(
validVotingResults, numberOfSeats, initialElectionNumber);
return SeatResult.builder()
.seatsPerResult(assignedSeatsPerResult)
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.build();
}
/**
* Finds all elected candidates for given voting results
*
* @param seatsPerNomination Seats to allocate per voting result
* @param electedCandidates Already elected candidates per party
*/
public ElectedResult calculateElectedOverallCandidates(
Map<VotingResult, Integer> seatsPerNomination,
Map<String, Collection<ElectedCandidate>> electedCandidates) {
log.info("Calculate overall elected candidates");
electionNumberHistory.clear();
return ElectedResult.builder()
.electedCandidates(findElectedCandidates(seatsPerNomination, electedCandidates))
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.build();
}
private static boolean passesVotingThreshold(@NonNull Election election, int totalVotes,
@NonNull VotingResult votingResult) {
return totalVotes * election.votingThreshold().getMultiplier()
<= votingResult.getTotalVotes();
}
/**
* Finds all elected candidates for given voting results.
*
* @param assignedSeatsPerNomination Map of seats to allocate for a voting result
* @param alreadyElectedCandidates Map of already elected candidates per party
*/
@NonNull
private Map<VotingResult, Collection<ElectedCandidate>> findElectedCandidates(
@NonNull Map<VotingResult, Integer> assignedSeatsPerNomination,
Map<String, Collection<ElectedCandidate>> alreadyElectedCandidates) {
Map<VotingResult, Collection<ElectedCandidate>> electedCandidates = new HashMap<>();
for (Entry<VotingResult, Integer> entry : assignedSeatsPerNomination.entrySet()) {
if (entry.getValue() > 0) {
Collection<ElectedCandidate> candidates = findCandidates(
entry.getKey(),
entry.getValue(),
alreadyElectedCandidates.getOrDefault(
entry.getKey().getNominationId().partyAbbreviation(),
Collections.emptyList())
);
electedCandidates.put(entry.getKey(), candidates);
}
}
return electedCandidates;
}
/**
* Finds elected candidates for given voting result.
*
* @param votingResult Result to search candidates for
* @param numberOfSeats Number of seats to allocate
* @param alreadyElectedCandidates Candidates already elected previously
*/
@NonNull
private Collection<ElectedCandidate> findCandidates(@NonNull VotingResult votingResult,
int numberOfSeats, @NonNull Collection<ElectedCandidate> alreadyElectedCandidates) {
log.info("Find elected candidates on nomination: {}",
votingResult.getNominationId().name());
List<Integer> individualVotesOrder = votingResult.getVotesPerPosition()
.entrySet().stream()
.sorted(Comparator.comparing(Entry<Integer, Integer>::getValue).reversed())
.map(Entry::getKey)
.toList();
Collection<ElectedCandidate> elected = new ArrayList<>();
int seatsByVoteOrder = numberOfSeats;
Map<Candidate, ElectedCandidate> electedCandidateMap = alreadyElectedCandidates.stream()
.collect(Collectors.toMap(ElectedCandidate::candidate, Function.identity()));
NominationId nominationId = votingResult.getNominationId();
Nomination nomination = nominationService.getNominationInternal(nominationId);
if (nomination.supportsVotesOnNomination()) {
int seatsByNomination = calculateSeatsByNominationOrder(votingResult, numberOfSeats);
seatsByVoteOrder = numberOfSeats - seatsByNomination;
List<Candidate> candidates = nomination.getCandidates();
int electedByNominationOrder = 0;
for (int i = 0; i < candidates.size() && electedByNominationOrder < seatsByNomination; i++) {
Candidate candidate = candidates.get(i);
ElectedCandidate electedCandidate = electedCandidateMap.get(candidate);
if (electedCandidate == null) {
electedCandidate = new ElectedCandidate(candidate, Elected.OVERALL_NOMINATION_ORDER);
elected.add(electedCandidate);
electedCandidateMap.put(candidate, electedCandidate);
electedByNominationOrder++;
}
}
}
int electedByVoteOrder = 0;
for (int i = 0; i < individualVotesOrder.size() && electedByVoteOrder < seatsByVoteOrder; i++) {
Integer position = individualVotesOrder.get(i);
Candidate candidate = nomination.getCandidate(position);
ElectedCandidate electedCandidate = electedCandidateMap.get(candidate);
if (electedCandidate == null) {
electedCandidate = new ElectedCandidate(candidate, nomination.supportsVotesOnNomination()
? Elected.OVERALL_VOTE_ORDER
: Elected.CONSTITUENCY);
elected.add(electedCandidate);
electedByVoteOrder++;
}
}
return elected;
}
private static int calculateSeatsByNominationOrder(@NonNull VotingResult votingResult,
int numberOfSeats) {
int votesOnNomination = votingResult.getVotesOnNomination();
int totalVotes = votingResult.getTotalVotesWithoutHealing();
return (int) Math.round((numberOfSeats * votesOnNomination) / (double) totalVotes);
}
private Map<VotingResult, Integer> calculateAssignedSeatsPerResult(
@NonNull Collection<VotingResult> votingResults,
int numberOfSeats, Double initialElectionNumber) {
double electionNumber = initialElectionNumber;
long assignedSeats;
Map<VotingResult, Integer> assignedSeatsPerVotingResult;
log.debug("Calculate assigned seats with initial election number {}", electionNumber);
do {
electionNumberHistory.add(electionNumber);
Map<VotingResult, Double> seatsPerVotingResult = new HashMap<>();
for (VotingResult votingResult : votingResults) {
double seatNumber = calculateAssignedSeatNumber(electionNumber,
votingResult.getTotalVotes());
seatsPerVotingResult.put(votingResult, seatNumber);
}
assignedSeatsPerVotingResult = seatsPerVotingResult.entrySet().stream()
.map(entry -> Map.entry(entry.getKey(), (int) Math.round(entry.getValue())))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
assignedSeats = assignedSeatsPerVotingResult.values().stream().reduce(0, Integer::sum);
if (assignedSeats < numberOfSeats) {
// election number was too big, decrease
electionNumber = calculateLowerElectionNumber(initialElectionNumber, seatsPerVotingResult,
assignedSeatsPerVotingResult);
log.debug("Calculated lower election number {}", electionNumber);
} else if (assignedSeats > numberOfSeats) {
// election number was too small, increase
electionNumber = calculateHigherElectionNumber(initialElectionNumber, seatsPerVotingResult,
assignedSeatsPerVotingResult);
log.debug("Calculated higher election number {}", electionNumber);
}
} while (assignedSeats != numberOfSeats);
return assignedSeatsPerVotingResult;
}
private static double calculateHigherElectionNumber(Double initialElectionNumber,
@NonNull Map<VotingResult, Double> seatsPerVotingResult,
Map<VotingResult, Integer> assignedSeatsPerVotingResult) {
double electionNumber;
electionNumber = seatsPerVotingResult.keySet().stream()
.map(votingResult -> votingResult.getTotalVotes()
/ (assignedSeatsPerVotingResult.get(votingResult) - 0.5))
.min(Comparator.comparing(Double::doubleValue))
.orElse(initialElectionNumber);
return electionNumber;
}
private static double calculateLowerElectionNumber(Double initialElectionNumber,
@NonNull Map<VotingResult, Double> seatsPerVotingResult,
Map<VotingResult, Integer> assignedSeatsPerVotingResult) {
double electionNumber;
electionNumber = seatsPerVotingResult.keySet().stream()
.map(votingResult -> votingResult.getTotalVotes()
/ (assignedSeatsPerVotingResult.get(votingResult) + 0.5))
.max(Comparator.comparing(Double::doubleValue))
.orElse(initialElectionNumber);
return electionNumber;
}
private double calculateAssignedSeatNumber(double electionNumber, int totalVotesOfNomination) {
return totalVotesOfNomination / electionNumber;
}
}
@@ -1,74 +0,0 @@
package de.twomartens.wahlrecht.service;
import de.twomartens.wahlrecht.mapper.v1.NominationMapper;
import de.twomartens.wahlrecht.model.db.Candidate;
import de.twomartens.wahlrecht.model.db.Nomination;
import de.twomartens.wahlrecht.model.internal.NominationId;
import de.twomartens.wahlrecht.repository.NominationRepository;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.mapstruct.factory.Mappers;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class NominationService {
private final NominationMapper mapper = Mappers.getMapper(NominationMapper.class);
private final NominationRepository repository;
private final CandidateService candidateService;
private Map<NominationId, Nomination> nominations;
public Nomination getNomination(NominationId id) {
if (nominations == null) {
fetchNominations();
}
return nominations.get(id);
}
public de.twomartens.wahlrecht.model.internal.Nomination getNominationInternal(NominationId id) {
return mapper.mapToInternal(getNomination(id));
}
public Nomination storeNomination(Nomination nomination) {
if (nomination == null) {
return null;
}
if (nominations == null) {
fetchNominations();
}
NominationId nominationId = new NominationId(nomination.getElectionName(),
nomination.getPartyAbbreviation(), nomination.getName());
Nomination existing = nominations.get(nominationId);
boolean needsUpdate = !nomination.equals(existing);
if (!needsUpdate) {
return existing;
}
Collection<Candidate> candidates = new ArrayList<>();
nomination.getCandidates()
.forEach(candidate -> candidates.add(candidateService.storeCandidate(candidate)));
if (existing != null) {
nomination = existing;
}
nomination.setCandidates(candidates);
nomination = repository.save(nomination);
nominations.put(nominationId, nomination);
return nomination;
}
private void fetchNominations() {
nominations = repository.findAll().stream()
.collect(Collectors.toMap(
n -> new NominationId(n.getElectionName(), n.getPartyAbbreviation(), n.getName()),
Function.identity()));
}
}
@@ -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)
}
}
@@ -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()
}
}
@@ -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
@@ -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()
}
}
@@ -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 {}
}
}
@@ -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)
@@ -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()
}
}
@@ -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
}
}
}
}
}
@@ -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()
}
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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>
}
@@ -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
)
}
}
@@ -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)
@@ -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;
}
@@ -0,0 +1,5 @@
package de.twomartens.wahlrecht.model.internal
data class ElectedCandidates(val overallResult: ElectedResult,
val constituencyResults: Map<Int, ElectedResult>,
val electionNumbersForSeatAllocation: List<Double>)
@@ -0,0 +1,10 @@
package de.twomartens.wahlrecht.model.internal
data class ElectedResult(
val electedCandidates: Map<VotingResult, Collection<ElectedCandidate>>,
val usedElectionNumbers: List<Double>
) {
val electedCandidatesByNomination = electedCandidates.entries.asSequence()
.map { Pair(it.key.nominationId, it.value) }
.toMap()
}
@@ -0,0 +1,7 @@
package de.twomartens.wahlrecht.model.internal
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,41 @@
package de.twomartens.wahlrecht.model.internal
data class Nomination(val id: NominationId, val supportVotesOnNomination: Boolean) {
private val candidates: MutableList<Candidate> = mutableListOf()
private val candidateToPosition: MutableMap<Candidate, Int> = mutableMapOf()
private val positionToCandidate: MutableMap<Int, Candidate> = mutableMapOf()
private fun addCandidate(candidate: Candidate) {
candidates.add(candidate)
candidateToPosition[candidate] = candidates.size
positionToCandidate[candidates.size] = candidate
}
fun addCandidates(vararg candidates: Candidate) {
candidates.forEach { addCandidate(it) }
}
fun addCandidates(candidates: Collection<Candidate>) {
candidates.forEach { addCandidate(it) }
}
fun getCandidates(): List<Candidate> {
return positionToCandidate.values.toList()
}
fun getCandidate(position: Int): Candidate? {
return positionToCandidate[position]
}
fun getPosition(candidate: Candidate): Int {
return candidateToPosition[candidate]!!
}
fun supportsVotesOnNomination(): Boolean {
return supportVotesOnNomination
}
override fun toString(): String {
return id.name
}
}

Some files were not shown because too many files have changed in this diff Show More