From 953a54998ef317930cff54e246c16b23fe3b441c Mon Sep 17 00:00:00 2001 From: Sandra Date: Fri, 28 Jan 2022 19:52:47 +0100 Subject: [PATCH] Create backend parser concept --- .../okaeri/timings/api/IndexController.java | 13 +++ .../timings/api/security/SecurityConfig.java | 36 ++++++++ .../timings/api/v1/ParseController.java | 90 +++++++++++++++++++ .../api/src/main/resources/application.yml | 8 +- 4 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 backend/api/src/main/java/eu/okaeri/timings/api/IndexController.java create mode 100644 backend/api/src/main/java/eu/okaeri/timings/api/security/SecurityConfig.java create mode 100644 backend/api/src/main/java/eu/okaeri/timings/api/v1/ParseController.java diff --git a/backend/api/src/main/java/eu/okaeri/timings/api/IndexController.java b/backend/api/src/main/java/eu/okaeri/timings/api/IndexController.java new file mode 100644 index 0000000..3bef1bf --- /dev/null +++ b/backend/api/src/main/java/eu/okaeri/timings/api/IndexController.java @@ -0,0 +1,13 @@ +package eu.okaeri.timings.api; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class IndexController { + + @RequestMapping + public String index() { + return "redirect:/swagger-ui"; + } +} diff --git a/backend/api/src/main/java/eu/okaeri/timings/api/security/SecurityConfig.java b/backend/api/src/main/java/eu/okaeri/timings/api/security/SecurityConfig.java new file mode 100644 index 0000000..4461924 --- /dev/null +++ b/backend/api/src/main/java/eu/okaeri/timings/api/security/SecurityConfig.java @@ -0,0 +1,36 @@ +package eu.okaeri.timings.api.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; +import java.util.List; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.httpBasic().disable(); + http.csrf().disable(); + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.setAllowedOrigins(List.of("*")); + config.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization")); + config.setAllowedMethods(List.of("*")); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/backend/api/src/main/java/eu/okaeri/timings/api/v1/ParseController.java b/backend/api/src/main/java/eu/okaeri/timings/api/v1/ParseController.java new file mode 100644 index 0000000..455777d --- /dev/null +++ b/backend/api/src/main/java/eu/okaeri/timings/api/v1/ParseController.java @@ -0,0 +1,90 @@ +package eu.okaeri.timings.api.v1; + +import lombok.SneakyThrows; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/v1/parse") +public class ParseController { + + @SneakyThrows + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity parse(@RequestPart("file") MultipartFile file) { + + if (!"text/csv".equals(file.getContentType())) { + return ResponseEntity.badRequest().body(Map.of("error", "Expected text/csv, got: " + file.getContentType())); + } + + String content = new String(file.getBytes(), StandardCharsets.UTF_8); + String[] lines = content.split("\r?\n"); + + Map metadata = new LinkedHashMap<>(); + List header = null; + List> records = new ArrayList<>(); + + for (String line : lines) { + + // metadata + if (line.startsWith("#")) { + if (!line.contains(":")) { + continue; + } + String[] parts = line.substring(1).split(":", 2); + if (parts.length != 2) { + throw new RuntimeException("Cannot parse metadata: '" + line + "'"); + } + String key = parts[0].trim().toLowerCase(Locale.ROOT); + String value = parts[1].trim(); + metadata.put(key, value); + continue; + } + + // header + if (header == null) { + String[] parts = line.split(","); + if (parts.length < 2) { + throw new RuntimeException("Cannot parse header: '" + line + "'"); + } + header = Arrays.asList(parts); + continue; + } + + // data + String[] parts = line.split(","); + if (parts.length != header.size()) { + throw new RuntimeException("Cannot parse record: '" + line + "'"); + } + + records.add(Arrays.stream(parts) + .map(value -> { + try { + return new BigDecimal(value); + } catch (Exception exception) { + throw new RuntimeException("Cannot parse value: '" + value + "' from record '" + line + "'"); + } + }) + .collect(Collectors.toList())); + } + + if (header == null || records.size() < 2) { + throw new RuntimeException("Invalid report"); + } + + return ResponseEntity.ok(Map.of( + "meta", metadata, + "header", header, + "data", records + )); + } +} diff --git a/backend/api/src/main/resources/application.yml b/backend/api/src/main/resources/application.yml index f37b063..ad95c23 100644 --- a/backend/api/src/main/resources/application.yml +++ b/backend/api/src/main/resources/application.yml @@ -20,9 +20,11 @@ springdoc: default-produces-media-type: "application/json" spring: - data: - rest: - base-path: /rest + servlet: + multipart: + enabled: true + max-file-size: 1MB + max-request-size: 1MB redis: host: 127.0.0.1 port: 6379