From f794725c6eae0d83bc68c48e4de008f7ff022553 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Sun, 8 Jan 2023 12:44:22 +0100
Subject: [PATCH] #724 - new input to configure the separator

---
 .../TransactionImportController.java          | 48 ++++++++++++-------
 .../transactions/csvImport/CsvImport.java     | 41 ++++++++++++++++
 .../resources/languages/base_de.properties    |  2 +
 .../resources/languages/base_en.properties    |  3 +-
 .../transactions/transactionImport.ftl        | 14 ++++--
 5 files changed, 87 insertions(+), 21 deletions(-)
 create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvImport.java

diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java
index 6877d55a6..a5b20376b 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java
@@ -2,19 +2,21 @@ package de.deadlocker8.budgetmaster.transactions;
 
 import de.deadlocker8.budgetmaster.controller.BaseController;
 import de.deadlocker8.budgetmaster.services.HelpersService;
+import de.deadlocker8.budgetmaster.transactions.csvImport.CsvImport;
 import de.deadlocker8.budgetmaster.transactions.csvImport.CsvParser;
 import de.deadlocker8.budgetmaster.transactions.csvImport.CsvRow;
 import de.deadlocker8.budgetmaster.utils.Mappings;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.context.request.RequestAttributes;
 import org.springframework.web.context.request.WebRequest;
-import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
 import java.nio.charset.StandardCharsets;
@@ -26,19 +28,20 @@ public class TransactionImportController extends BaseController
 {
 	private static class ModelAttributes
 	{
+		public static final String ERROR = "error";
 		public static final String ERROR_UPLOAD = "errorUpload";
 	}
 
 	private static class ReturnValues
 	{
 		public static final String TRANSACTION_IMPORT = "transactions/transactionImport";
+		public static final String REDIRECT_IMPORT = "redirect:/transactionImport";
 		public static final String REDIRECT_CANCEL = "redirect:/transactionImport/cancel";
-
 	}
 
 	private static class RequestAttributeNames
 	{
-		public static final String IMPORTED_FILE = "importedFile";
+		public static final String CSV_IMPORT = "csvImport";
 		public static final String CSV_ROWS = "csvRows";
 	}
 
@@ -55,35 +58,46 @@ public class TransactionImportController extends BaseController
 	@GetMapping
 	public String transactionImport(HttpServletRequest request, Model model)
 	{
+		model.addAttribute(RequestAttributeNames.CSV_IMPORT, new CsvImport(null, ";"));
 		return ReturnValues.TRANSACTION_IMPORT;
 	}
 
 	@PostMapping("/upload")
-	public String upload(WebRequest request, Model model, @RequestParam("file") MultipartFile file)
+	public String upload(WebRequest request,
+						 Model model,
+						 @ModelAttribute("CsvImport") CsvImport csvImport,
+						 BindingResult bindingResult)
 	{
-		if(file.isEmpty())
+		if(csvImport.file().isEmpty())
 		{
 			return ReturnValues.REDIRECT_CANCEL;
 		}
 
-		try
+		if(!csvImport.isValidSeparator())
 		{
-			final String csvString = new String(file.getBytes(), StandardCharsets.UTF_8);
-			final List<CsvRow> csvRows = CsvParser.parseCsv(csvString, ';');
+			bindingResult.addError(new FieldError("CsvImport", "separator", "", false, new String[]{"warning.transaction.import.separator"}, null, null));
+		}
 
-			String fileName = file.getOriginalFilename();
-			if(fileName == null)
-			{
-				fileName = file.getName();
-			}
+		if(bindingResult.hasErrors())
+		{
+			model.addAttribute(ModelAttributes.ERROR, bindingResult);
+			request.setAttribute(RequestAttributeNames.CSV_IMPORT, csvImport, RequestAttributes.SCOPE_SESSION);
+			return ReturnValues.TRANSACTION_IMPORT;
+		}
 
-			request.setAttribute(RequestAttributeNames.IMPORTED_FILE, fileName, RequestAttributes.SCOPE_SESSION);
+		try
+		{
+			final String csvString = new String(csvImport.file().getBytes(), StandardCharsets.UTF_8);
+			final List<CsvRow> csvRows = CsvParser.parseCsv(csvString, csvImport.separator().charAt(0));
+
+			request.setAttribute(RequestAttributeNames.CSV_IMPORT, csvImport, RequestAttributes.SCOPE_SESSION);
 			request.setAttribute(RequestAttributeNames.CSV_ROWS, csvRows, RequestAttributes.SCOPE_SESSION);
 		}
 		catch(Exception e)
 		{
 			LOGGER.error("CSV upload failed", e);
 
+			// TODO: show in html
 			model.addAttribute(ModelAttributes.ERROR_UPLOAD, e.getMessage());
 		}
 
@@ -93,9 +107,9 @@ public class TransactionImportController extends BaseController
 	@GetMapping("/cancel")
 	public String cancel(WebRequest request)
 	{
-		request.removeAttribute(RequestAttributeNames.IMPORTED_FILE, RequestAttributes.SCOPE_SESSION);
+		request.removeAttribute(RequestAttributeNames.CSV_IMPORT, RequestAttributes.SCOPE_SESSION);
 		request.removeAttribute(RequestAttributeNames.CSV_ROWS, RequestAttributes.SCOPE_SESSION);
 
-		return ReturnValues.TRANSACTION_IMPORT;
+		return ReturnValues.REDIRECT_IMPORT;
 	}
 }
\ No newline at end of file
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvImport.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvImport.java
new file mode 100644
index 000000000..c5e944900
--- /dev/null
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvImport/CsvImport.java
@@ -0,0 +1,41 @@
+package de.deadlocker8.budgetmaster.transactions.csvImport;
+
+import org.springframework.web.multipart.MultipartFile;
+
+public record CsvImport(MultipartFile file, String separator)
+{
+	@Override
+	public String toString()
+	{
+		return "CsvImport{" +
+				"file=" + file +
+				", separator='" + separator + '\'' +
+				'}';
+	}
+
+	public boolean isValidSeparator()
+	{
+		if(separator == null)
+		{
+			return false;
+		}
+
+		return separator.strip().length() == 1;
+	}
+
+	public String getFileName()
+	{
+		if(file == null)
+		{
+			return null;
+		}
+
+		final String fileName = file.getOriginalFilename();
+		if(fileName == null)
+		{
+			return file.getName();
+		}
+
+		return fileName;
+	}
+}
diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties
index f6aa6df88..1f0e210fd 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_de.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties
@@ -232,6 +232,7 @@ warning.empty.git.url=Bitte gib die URL zum git-Server ein.
 warning.empty.git.branch.name=Bitte gib den Namen des git-Branches ein.
 warning.empty.git.user.name=Bitte gib deinen git-Nutzernamen ein.
 warning.empty.git.token=Bitte gib dein git-Zugriffstoken ein.
+warning.transaction.import.separator=Ungültiges Trennzeichen. Bitte genau ein Zeichen eingeben.
 
 
 # UI
@@ -370,6 +371,7 @@ transactions.recurring.headline=Aktive wiederholende Buchungen
 transactions.recurring.placeholder=Keine aktiven wiederholenden Buchungen
 transactions.import.overview=Übersicht
 transactions.import.column=Spalte
+transactions.import.separator=Trennzeichen
 
 repeating.button.add=Wiederholung hinzufügen
 repeating.button.remove=Wiederholung entfernen
diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties
index 79df3e632..9d7e57ce5 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_en.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties
@@ -21,7 +21,6 @@ error.database.import.invalid.json=The uploaded JSON File is invalid.
 error.database.import.unknown.version=The uploaded JSON File does not contain a valid BudgetMaster database definition.
 error.database.import.version.too.old=The uploaded JSON File is too old. Version: {0} Minimum version: {1}
 
-
 # TITLE
 title.incomes=Incomes
 title.income=Income
@@ -233,6 +232,7 @@ warning.empty.git.url=Please insert the git server's URL.
 warning.empty.git.branch.name=Please insert the git branch name.
 warning.empty.git.user.name=Please insert your git username.
 warning.empty.git.token=Please insert your git access token.
+warning.transaction.import.separator=Invalid separator. Please enter exactly one character.
 
 
 # UI
@@ -370,6 +370,7 @@ transactions.recurring.headline=Active Recurring Transactions
 transactions.recurring.placeholder=No active recurring transactions
 transactions.import.overview=Overview
 transactions.import.column=Column
+transactions.import.separator=Separator
 
 repeating.button.add=Add repetition
 repeating.button.remove=Remove repetition
diff --git a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl
index 41cac7672..cd7c01f51 100644
--- a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImport.ftl
@@ -1,6 +1,7 @@
 <html>
     <head>
         <#import "../helpers/header.ftl" as header>
+        <#import "../helpers/validation.ftl" as validation>
         <@header.globals/>
         <@header.header "BudgetMaster - ${locale.getString('menu.transactions.import')}"/>
         <#import "/spring.ftl" as s>
@@ -21,10 +22,10 @@
 
                 <@header.content>
                     <div class="container">
-                        <#if importedFile??>
+                        <#if !error?? && csvImport.getFileName()??>
                             <div class="row center-align">
                                 <div class="col s12 m12 l8 offset-l2 headline-small text-green">
-                                    <i class="fas fa-file-csv"></i> ${importedFile}
+                                    <i class="fas fa-file-csv"></i> ${csvImport.getFileName()}
                                 </div>
                             </div>
 
@@ -52,7 +53,7 @@
 </html>
 
 <#macro csvUpload>
-    <form id="form-csv-import" method="POST" action="<@s.url '/transactionImport/upload'/>" enctype="multipart/form-data" accept-charset="UTF-8">
+    <form id="form-csv-import" name="CsvImport" method="POST" action="<@s.url '/transactionImport/upload'/>" enctype="multipart/form-data" accept-charset="UTF-8">
         <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 
         <div class="row">
@@ -67,6 +68,13 @@
             </div>
         </div>
 
+        <div class="row">
+            <div class="input-field col s2 offset-s5">
+                <input id="separator" type="text" name="separator" <@validation.validation "separator" "center-align"/> value="<#if csvImport??>${csvImport.separator()}</#if>">
+                <label class="input-label" for="separator">${locale.getString("transactions.import.separator")}</label>
+            </div>
+        </div>
+
         <div class="row">
             <div class="col s12 center-align">
                 <@header.buttonSubmit name='action' icon='cloud_upload' localizationKey='settings.database.import' id='button-confirm-csv-import' classes='text-white'/>
-- 
GitLab