From fba9caf9de6da62c368f86471938bd5fdf7be644 Mon Sep 17 00:00:00 2001 From: emrecam Date: Tue, 9 Jul 2024 15:21:17 +0300 Subject: [PATCH] :sparkles: Add column renaming support for saved csv content --- .../server/endpoint/CodeSystemEndpoint.scala | 30 ++++++++++---- .../server/endpoint/ConceptMapEndpoint.scala | 29 +++++++++---- .../endpoint/MappingContextEndpoint.scala | 29 +++++++++---- .../tofhir/server/model/csv/CsvHeader.scala | 9 ++++ .../IMappingContextRepository.scala | 3 +- .../MappingContextFolderRepository.scala | 3 +- .../codesystem/CodeSystemRepository.scala | 3 +- .../codesystem/ICodeSystemRepository.scala | 3 +- .../conceptmap/ConceptMapRepository.scala | 3 +- .../conceptmap/IConceptMapRepository.scala | 3 +- .../service/MappingContextService.scala | 3 +- .../terminology/CodeSystemService.scala | 3 +- .../terminology/ConceptMapService.scala | 3 +- .../scala/io/tofhir/server/util/CsvUtil.scala | 41 ++++++++++++------- 14 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 tofhir-server/src/main/scala/io/tofhir/server/model/csv/CsvHeader.scala diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/CodeSystemEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/CodeSystemEndpoint.scala index 2d36d99e..cf214e7d 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/CodeSystemEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/CodeSystemEndpoint.scala @@ -10,6 +10,7 @@ import io.tofhir.server.common.model.{ResourceNotFound, ToFhirRestCall} import io.tofhir.server.endpoint.CodeSystemEndpoint.SEGMENT_CODE_SYSTEMS import io.tofhir.server.endpoint.TerminologyServiceManagerEndpoint._ import io.tofhir.common.model.Json4sSupport._ +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.service.terminology.CodeSystemService import io.tofhir.server.repository.terminology.codesystem.ICodeSystemRepository @@ -73,18 +74,29 @@ class CodeSystemEndpoint(codeSystemRepository: ICodeSystemRepository) extends La } /** - * Route to update code system csv headers - * Headers are passed as a list of strings and overwrite the existing headers in the first line of the CSV file - * Compare existing columns names are new names and adjust the rows to match the new headers if necessary + * Route to update code system CSV headers. + * + * Headers are passed as a list of `CsvHeader` objects, where each `CsvHeader` contains both the `currentName` (new header name) and `previousName` (the previously saved header name). + * The existing headers in the first line of the CSV file are overwritten with the new headers provided. The rows are adjusted to match the new headers if necessary. * * e.g. 1: - * Existing headers in a CSV: "header1", "header2" --> New headers: "header1", "header2", "header3" - * Rows belonging to the header1 and header2 are preserved and only header3 is added with a default value for each row () + * Existing headers in a CSV: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "header2", previousName = "header2") + * New headers: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "headerChanged", previousName = "header2") + * - CsvHeader(currentName = "header3", previousName = "header3") + * Rows under "header1" are preserved, rows belonging to "header2" are moved to "headerChanged", and "header3" is added with a default value for each row (``). * * e.g. 2: - * Existing headers in a CSV: "header1", "header2" --> New headers: "header2", "header3" - * Rows belonging to the header1 are removed, header2 is preserved and shifted to the first column with its values - * and header3 is added as second column with a default value for each row () + * Existing headers in a CSV: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "header2", previousName = "header2") + * New headers: + * - CsvHeader(currentName = "header2", previousName = "header2") + * - CsvHeader(currentName = "header3", previousName = "header3") + * Rows under "header1" are removed, rows under "header2" are preserved and shifted to the first column, and "header3" is added as the second column with a default value for each row (``). * * @param terminologyId terminology id * @param codeSystemId code system id @@ -92,7 +104,7 @@ class CodeSystemEndpoint(codeSystemRepository: ICodeSystemRepository) extends La */ private def updateCodeSystemHeaderRoute(terminologyId: String, codeSystemId: String): Route = { post { - entity(as[Seq[String]]) { headers => + entity(as[Seq[CsvHeader]]) { headers => complete { service.updateCodeSystemHeader(terminologyId, codeSystemId, headers) map { _ => StatusCodes.OK diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ConceptMapEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ConceptMapEndpoint.scala index 14faa0a1..0ee3a209 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ConceptMapEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ConceptMapEndpoint.scala @@ -10,6 +10,7 @@ import io.tofhir.server.common.model.{ResourceNotFound, ToFhirRestCall} import io.tofhir.server.endpoint.ConceptMapEndpoint.SEGMENT_CONCEPT_MAPS import io.tofhir.server.endpoint.TerminologyServiceManagerEndpoint._ import io.tofhir.common.model.Json4sSupport._ +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.service.terminology.ConceptMapService import io.tofhir.server.repository.terminology.conceptmap.IConceptMapRepository @@ -75,24 +76,36 @@ class ConceptMapEndpoint(conceptMapRepository: IConceptMapRepository) extends La /** * Route to update concept map csv header * - * Headers are passed as a list of strings and overwrite the existing headers in the first line of the CSV file - * Compare existing columns names are new names and adjust the rows to match the new headers if necessary + * Headers are passed as a list of `CsvHeader` objects and overwrite the existing headers in the first line of the CSV file. + * The `CsvHeader` class represents each header with both its current name and previously saved name. + * The rows are adjusted to match the new headers if necessary. * * e.g. 1: - * Existing headers in a CSV: "header1", "header2" --> New headers: "header1", "header2", "header3" - * Rows belonging to the header1 and header2 are preserved and only header3 is added with a default value for each row () + * Existing headers in a CSV: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "header2", previousName = "header2") + * New headers: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "headerChanged", previousName = "header2") + * - CsvHeader(currentName = "header3", previousName = "header3") + * Rows under "header1" are preserved, rows belonging to "header2" are moved to "headerChanged", and "header3" is added with a default value for each row (``). * * e.g. 2: - * Existing headers in a CSV: "header1", "header2" --> New headers: "header2", "header3" - * Rows belonging to the header1 are removed, header2 is preserved and shifted to the first column with its values - * and header3 is added as second column with a default value for each row () + * Existing headers in a CSV: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "header2", previousName = "header2") + * New headers: + * - CsvHeader(currentName = "header2", previousName = "header2") + * - CsvHeader(currentName = "header3", previousName = "header3") + * Rows under "header1" are removed, rows under "header2" are preserved and shifted to the first column, and "header3" is added as the second column with a default value for each row (``). + * * @param terminologyId terminology id * @param conceptMapId concept map id * @return */ private def updateConceptMapHeaderRoute(terminologyId: String, conceptMapId: String): Route = { post { - entity(as[Seq[String]]) { headers => + entity(as[Seq[CsvHeader]]) { headers => complete { service.updateConceptMapHeader(terminologyId, conceptMapId, headers) map { _ => StatusCodes.OK diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingContextEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingContextEndpoint.scala index 4cbc49d7..661c1496 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingContextEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingContextEndpoint.scala @@ -9,6 +9,7 @@ import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.common.model.ToFhirRestCall import io.tofhir.server.endpoint.MappingContextEndpoint.{ATTACHMENT, SEGMENT_CONTENT, SEGMENT_CONTEXTS, SEGMENT_FILE, SEGMENT_HEADER} import io.tofhir.common.model.Json4sSupport._ +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.repository.mappingContext.IMappingContextRepository import io.tofhir.server.service.MappingContextService @@ -91,24 +92,36 @@ class MappingContextEndpoint(mappingContextRepository: IMappingContextRepository /** * Route to update the mapping context CSV headers * - * Headers are passed as a list of strings and overwrite the existing headers in the first line of the CSV file - * Compare existing columns names are new names and adjust the rows to match the new headers if necessary + * Headers are passed as a list of `CsvHeader` objects and overwrite the existing headers in the first line of the CSV file. + * The `CsvHeader` class represents each header with both its current name and previously saved name. + * The rows are adjusted to match the new headers if necessary. * * e.g. 1: - * Existing headers in a CSV: "header1", "header2" --> New headers: "header1", "header2", "header3" - * Rows belonging to the header1 and header2 are preserved and only header3 is added with a default value for each row () + * Existing headers in a CSV: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "header2", previousName = "header2") + * New headers: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "headerChanged", previousName = "header2") + * - CsvHeader(currentName = "header3", previousName = "header3") + * Rows under "header1" are preserved, rows belonging to "header2" are moved to "headerChanged", and "header3" is added with a default value for each row (``). * * e.g. 2: - * Existing headers in a CSV: "header1", "header2" --> New headers: "header2", "header3" - * Rows belonging to the header1 are removed, header2 is preserved and shifted to the first column with its values - * and header3 is added as second column with a default value for each row () + * Existing headers in a CSV: + * - CsvHeader(currentName = "header1", previousName = "header1") + * - CsvHeader(currentName = "header2", previousName = "header2") + * New headers: + * - CsvHeader(currentName = "header2", previousName = "header2") + * - CsvHeader(currentName = "header3", previousName = "header3") + * Rows under "header1" are removed, rows under "header2" are preserved and shifted to the first column, and "header3" is added as the second column with a default value for each row (``). + * * @param projectId project id * @param id mapping context id * @return */ private def updateMappingContextHeaderRoute(projectId: String, id: String): Route = { post { - entity(as[Seq[String]]) { headers => + entity(as[Seq[CsvHeader]]) { headers => complete { service.updateMappingContextHeader(projectId, id, headers) map { _ => StatusCodes.OK diff --git a/tofhir-server/src/main/scala/io/tofhir/server/model/csv/CsvHeader.scala b/tofhir-server/src/main/scala/io/tofhir/server/model/csv/CsvHeader.scala new file mode 100644 index 00000000..0f508e18 --- /dev/null +++ b/tofhir-server/src/main/scala/io/tofhir/server/model/csv/CsvHeader.scala @@ -0,0 +1,9 @@ +package io.tofhir.server.model.csv + +/** + * Represents a CSV header with a new name and a previously saved name. + * @param currentName The current name of the header. + * @param previousName The previously saved name of the header, used for column name change without data loss + * If a new header is desired to be created, saved name may be any placeholder + */ +case class CsvHeader(currentName: String, previousName: String) \ No newline at end of file diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/mappingContext/IMappingContextRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/mappingContext/IMappingContextRepository.scala index 49c3aa2a..33c4cb36 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/mappingContext/IMappingContextRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/mappingContext/IMappingContextRepository.scala @@ -2,6 +2,7 @@ package io.tofhir.server.repository.mappingContext import akka.stream.scaladsl.Source import akka.util.ByteString +import io.tofhir.server.model.csv.CsvHeader import scala.concurrent.Future @@ -48,7 +49,7 @@ trait IMappingContextRepository { * @param headers mapping context headers * @return */ - def updateMappingContextHeader(projectId: String, id: String, headers: Seq[String]): Future[Unit] + def updateMappingContextHeader(projectId: String, id: String, headers: Seq[CsvHeader]): Future[Unit] /** * Save the mapping context content to the repository diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/mappingContext/MappingContextFolderRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/mappingContext/MappingContextFolderRepository.scala index fea02c81..ff1882f9 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/mappingContext/MappingContextFolderRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/mappingContext/MappingContextFolderRepository.scala @@ -7,6 +7,7 @@ import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.util.FileUtils import io.tofhir.server.common.model.{AlreadyExists, ResourceNotFound} import io.tofhir.server.model._ +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.repository.project.ProjectFolderRepository import io.tofhir.server.util.CsvUtil @@ -114,7 +115,7 @@ class MappingContextFolderRepository(mappingContextRepositoryFolderPath: String, * @param headers mapping context headers * @return */ - def updateMappingContextHeader(projectId: String, id: String, headers: Seq[String]): Future[Unit] = { + def updateMappingContextHeader(projectId: String, id: String, headers: Seq[CsvHeader]): Future[Unit] = { if (!mappingContextExists(projectId, id)) { throw ResourceNotFound("Mapping context does not exists.", s"A mapping context with id $id does not exists in the mapping context repository at ${FileUtils.getPath(mappingContextRepositoryFolderPath).toAbsolutePath.toString}") } diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/CodeSystemRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/CodeSystemRepository.scala index da75f288..d0582e29 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/CodeSystemRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/CodeSystemRepository.scala @@ -8,6 +8,7 @@ import io.tofhir.engine.util.FileUtils import io.tofhir.server.common.model.{AlreadyExists, BadRequest, ResourceNotFound} import io.tofhir.server.model.TerminologySystem.TerminologyCodeSystem import io.tofhir.server.model._ +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.repository.terminology.TerminologySystemFolderRepository.getTerminologySystemsJsonPath import io.tofhir.server.util.{CsvUtil, FileOperations} import org.json4s.jackson.Serialization.writePretty @@ -166,7 +167,7 @@ class CodeSystemRepository(terminologySystemFolderPath: String) extends ICodeSys * @param headers new headers to update * @return */ - def updateCodeSystemHeader(terminologyId: String, codeSystemId: String, headers: Seq[String]): Future[Unit] = { + def updateCodeSystemHeader(terminologyId: String, codeSystemId: String, headers: Seq[CsvHeader]): Future[Unit] = { val codeSystem = findCodeSystemById(terminologyId, codeSystemId) // get file and update headers val codeSystemFile = FileUtils.getPath(terminologySystemFolderPath, terminologyId, codeSystem.id).toFile diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/ICodeSystemRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/ICodeSystemRepository.scala index ff4fa1bd..5e79300b 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/ICodeSystemRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/ICodeSystemRepository.scala @@ -3,6 +3,7 @@ package io.tofhir.server.repository.terminology.codesystem import akka.stream.scaladsl.Source import akka.util.ByteString import io.tofhir.server.model.TerminologySystem.TerminologyCodeSystem +import io.tofhir.server.model.csv.CsvHeader import scala.concurrent.Future @@ -53,7 +54,7 @@ trait ICodeSystemRepository { * @param headers new headers to update * @return */ - def updateCodeSystemHeader(terminologyId: String, codeSystemId: String, headers: Seq[String]): Future[Unit] + def updateCodeSystemHeader(terminologyId: String, codeSystemId: String, headers: Seq[CsvHeader]): Future[Unit] /** * Retrieve and save the content of a code system csv file within a terminology diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/ConceptMapRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/ConceptMapRepository.scala index e4094244..93e487a5 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/ConceptMapRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/ConceptMapRepository.scala @@ -9,6 +9,7 @@ import io.tofhir.engine.util.FileUtils import io.tofhir.server.common.model.{AlreadyExists, BadRequest, InternalError, ResourceNotFound} import io.tofhir.server.model.TerminologySystem.TerminologyConceptMap import io.tofhir.server.model._ +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.repository.terminology.TerminologySystemFolderRepository.getTerminologySystemsJsonPath import io.tofhir.server.util.{CsvUtil, FileOperations} import org.json4s.jackson.Serialization.writePretty @@ -168,7 +169,7 @@ class ConceptMapRepository(terminologySystemFolderPath: String) extends IConcept * @param headers new headers to update * @return */ - def updateConceptMapHeader(terminologyId: String, conceptMapId: String, headers: Seq[String]): Future[Unit] = { + def updateConceptMapHeader(terminologyId: String, conceptMapId: String, headers: Seq[CsvHeader]): Future[Unit] = { val conceptMap = findConceptMapById(terminologyId, conceptMapId) // get file and update headers val conceptMapFile = FileUtils.getPath(terminologySystemFolderPath, terminologyId, conceptMap.id).toFile diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/IConceptMapRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/IConceptMapRepository.scala index a50c378a..c80eac01 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/IConceptMapRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/IConceptMapRepository.scala @@ -3,6 +3,7 @@ package io.tofhir.server.repository.terminology.conceptmap import akka.stream.scaladsl.Source import akka.util.ByteString import io.tofhir.server.model.TerminologySystem.TerminologyConceptMap +import io.tofhir.server.model.csv.CsvHeader import scala.concurrent.Future @@ -53,7 +54,7 @@ trait IConceptMapRepository { * @param headers new headers to update * @return */ - def updateConceptMapHeader(terminologyId: String, conceptMapId: String, headers: Seq[String]): Future[Unit] + def updateConceptMapHeader(terminologyId: String, conceptMapId: String, headers: Seq[CsvHeader]): Future[Unit] /** * Retrieve and save the content of a concept map csv file within a terminology diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/MappingContextService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/MappingContextService.scala index fd7601b7..44c25c8f 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/MappingContextService.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/service/MappingContextService.scala @@ -3,6 +3,7 @@ package io.tofhir.server.service import akka.stream.scaladsl.Source import akka.util.ByteString import com.typesafe.scalalogging.LazyLogging +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.repository.mappingContext.IMappingContextRepository import scala.concurrent.Future @@ -49,7 +50,7 @@ class MappingContextService(mappingContextRepository: IMappingContextRepository) * @param headers mapping context headers * @return */ - def updateMappingContextHeader(projectId: String, id: String, headers: Seq[String]): Future[Unit] = { + def updateMappingContextHeader(projectId: String, id: String, headers: Seq[CsvHeader]): Future[Unit] = { mappingContextRepository.updateMappingContextHeader(projectId, id, headers) } diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/terminology/CodeSystemService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/terminology/CodeSystemService.scala index d65bba8b..9cf22931 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/terminology/CodeSystemService.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/service/terminology/CodeSystemService.scala @@ -4,6 +4,7 @@ import akka.stream.scaladsl.Source import akka.util.ByteString import com.typesafe.scalalogging.LazyLogging import io.tofhir.server.model.TerminologySystem.TerminologyCodeSystem +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.repository.terminology.codesystem.ICodeSystemRepository import scala.concurrent.Future @@ -67,7 +68,7 @@ class CodeSystemService(codeSystemRepository: ICodeSystemRepository) extends Laz * @param headers new headers to update * @return */ - def updateCodeSystemHeader(terminologyId: String, codeSystemId: String, headers: Seq[String]): Future[Unit] = { + def updateCodeSystemHeader(terminologyId: String, codeSystemId: String, headers: Seq[CsvHeader]): Future[Unit] = { codeSystemRepository.updateCodeSystemHeader(terminologyId, codeSystemId, headers) } diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/terminology/ConceptMapService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/terminology/ConceptMapService.scala index 51fd56a7..ba5296e3 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/terminology/ConceptMapService.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/service/terminology/ConceptMapService.scala @@ -4,6 +4,7 @@ import akka.stream.scaladsl.Source import akka.util.ByteString import com.typesafe.scalalogging.LazyLogging import io.tofhir.server.model.TerminologySystem.TerminologyConceptMap +import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.repository.terminology.conceptmap.IConceptMapRepository import scala.concurrent.Future @@ -67,7 +68,7 @@ class ConceptMapService(conceptMapRepository: IConceptMapRepository) extends Laz * @param headers new headers to update * @return */ - def updateConceptMapHeader(terminologyId: String, conceptMapId: String, headers: Seq[String]): Future[Unit] = { + def updateConceptMapHeader(terminologyId: String, conceptMapId: String, headers: Seq[CsvHeader]): Future[Unit] = { conceptMapRepository.updateConceptMapHeader(terminologyId, conceptMapId, headers) } diff --git a/tofhir-server/src/main/scala/io/tofhir/server/util/CsvUtil.scala b/tofhir-server/src/main/scala/io/tofhir/server/util/CsvUtil.scala index b6d78f62..528dc232 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/util/CsvUtil.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/util/CsvUtil.scala @@ -6,6 +6,7 @@ import com.opencsv.CSVParserBuilder import io.tofhir.engine.Execution.actorSystem import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.common.model.InternalError +import io.tofhir.server.model.csv.CsvHeader import java.io.File import java.nio.file.StandardOpenOption @@ -50,6 +51,7 @@ object CsvUtil { /** * Overwrite headers string to the first line of the CSV file * Adjust the rows to match the new headers if necessary e.g. + * - if a column name is changed, change headers of the rows with new name * - if a column removed, remove the column from the rows * - if a new column name recognized, add the column to the rows with a default value () * - order of the columns are preserved according to the newHHeaders @@ -57,7 +59,7 @@ object CsvUtil { * @param newHeaders Headers to write to the file * @return */ - def writeCsvHeaders(file: File, newHeaders: Seq[String]): Future[Unit] = { + def writeCsvHeaders(file: File, newHeaders: Seq[CsvHeader]): Future[Unit] = { // Used external CSV Parser library to handle double quotes in the CSV file val parser = new CSVParserBuilder().withSeparator(',').withQuoteChar('"').build() @@ -79,34 +81,44 @@ object CsvUtil { } } + // here an example existingContent looks like: - // [ - // [ "header1" -> "value1", "header2" -> "value2" ], - // [ "header1" -> "value3", "header2" -> "value4" ] - // ] + // header1, header2, header3 + // value1, value2, value3 + // value4, value5, value6 // // an example newHeaders looks like: - // [ "header2", "header3" ] + // Seq( + // CsvHeader(currentName = "header2", previousName = "header2"), + // CsvHeader(currentName = "headerChanged", previousName = "header3"), + // CsvHeader(currentName = "header4", previousName = "header4") // previousName may be any placeholder + // ) + // "header1" is deleted, + // "header3" changed to "headerChanged" + // "header4" is added existingContentFuture.map { existingContent => // Create a new list of lists where each list is a row and the first element in the tuples is the header val updatedContent = existingContent.map { row => // iterate each row newHeaders.map { header => // iterate each new header - // if the header already exists, use its value (header2 -> value2) - // otherwise, use a default value (header3 -> ) - header -> row.find(_._1 == header).map(_._2).getOrElse(s"<$header>") + // If the current name is different than the previously saved name, update the header of tuples + val key = if (header.currentName != header.previousName) header.currentName else header.previousName + // if the header already exists or header name is changed, use its value (header2 -> value2, headerChanged -> value3) + // otherwise, use a default value (header4 -> ) + key -> row.find(_._1 == header.previousName).map(_._2).getOrElse(s"<${header.currentName}>") } } // here an example updatedContent looks like: - // [ - // [ "header2" -> "value2", "header3" -> "" ], - // [ "header2" -> "value4", "header3" -> "" ] - // ] + // header2, headerChanged, header4 + // value2, value3, + // value5, value6, + // Extract the new names from CsvHeader array + val newHeaderNames = newHeaders.map(_.currentName) // create a header line by joining the new headers with a comma // create row content by using the second element of the tuple and joining them with a comma // finally merge the header and row content with a comma - val csvContent = (newHeaders.mkString(",") +: updatedContent.map(_.map(x => s"\"${x._2}\"").mkString(","))).map(ByteString(_)) + val csvContent = (newHeaderNames.mkString(",") +: updatedContent.map(_.map(x => s"\"${x._2}\"").mkString(","))).map(ByteString(_)) // Write the updated CSV content back to the file Source(csvContent).intersperse(ByteString("\n")) .runWith(FileIO.toPath(file.toPath, Set(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING))) @@ -205,5 +217,4 @@ object CsvUtil { .recover(e => throw InternalError("Error while writing file.", e.getMessage)) } - }