Skip to content

Commit

Permalink
✨ Add column renaming support for saved csv content
Browse files Browse the repository at this point in the history
  • Loading branch information
camemre49 authored and dogukan10 committed Aug 6, 2024
1 parent 01c7485 commit 936d487
Show file tree
Hide file tree
Showing 13 changed files with 52 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.tofhir.server.endpoint.TerminologyServiceManagerEndpoint._
import io.tofhir.common.model.Json4sSupport._
import io.tofhir.server.service.terminology.CodeSystemService
import io.tofhir.server.repository.terminology.codesystem.ICodeSystemRepository
import io.tofhir.server.util.CsvUtil.CsvHeader

class CodeSystemEndpoint(codeSystemRepository: ICodeSystemRepository) extends LazyLogging {

Expand Down Expand Up @@ -92,7 +93,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.tofhir.server.endpoint.TerminologyServiceManagerEndpoint._
import io.tofhir.common.model.Json4sSupport._
import io.tofhir.server.service.terminology.ConceptMapService
import io.tofhir.server.repository.terminology.conceptmap.IConceptMapRepository
import io.tofhir.server.util.CsvUtil.CsvHeader

class ConceptMapEndpoint(conceptMapRepository: IConceptMapRepository) extends LazyLogging {

Expand Down Expand Up @@ -92,7 +93,7 @@ class ConceptMapEndpoint(conceptMapRepository: IConceptMapRepository) extends La
*/
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.tofhir.server.endpoint.MappingContextEndpoint.{ATTACHMENT, SEGMENT_CON
import io.tofhir.common.model.Json4sSupport._
import io.tofhir.server.repository.mappingContext.IMappingContextRepository
import io.tofhir.server.service.MappingContextService
import io.tofhir.server.util.CsvUtil.CsvHeader

class MappingContextEndpoint(mappingContextRepository: IMappingContextRepository) extends LazyLogging {

Expand Down Expand Up @@ -108,7 +109,7 @@ class MappingContextEndpoint(mappingContextRepository: IMappingContextRepository
*/
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.tofhir.server.repository.mappingContext

import akka.stream.scaladsl.Source
import akka.util.ByteString
import io.tofhir.server.util.CsvUtil.CsvHeader

import scala.concurrent.Future

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.tofhir.server.common.model.{AlreadyExists, ResourceNotFound}
import io.tofhir.server.model._
import io.tofhir.server.repository.project.ProjectFolderRepository
import io.tofhir.server.util.CsvUtil

import io.tofhir.server.util.CsvUtil.CsvHeader
import java.io.File
import scala.collection.mutable
import scala.concurrent.Future
Expand Down Expand Up @@ -114,7 +114,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}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.tofhir.server.common.model.{AlreadyExists, BadRequest, ResourceNotFoun
import io.tofhir.server.model.TerminologySystem.TerminologyCodeSystem
import io.tofhir.server.model._
import io.tofhir.server.repository.terminology.TerminologySystemFolderRepository.getTerminologySystemsJsonPath
import io.tofhir.server.util.CsvUtil.CsvHeader
import io.tofhir.server.util.{CsvUtil, FileOperations}
import org.json4s.jackson.Serialization.writePretty

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.util.CsvUtil.CsvHeader

import scala.concurrent.Future

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.tofhir.server.common.model.{AlreadyExists, BadRequest, InternalError,
import io.tofhir.server.model.TerminologySystem.TerminologyConceptMap
import io.tofhir.server.model._
import io.tofhir.server.repository.terminology.TerminologySystemFolderRepository.getTerminologySystemsJsonPath
import io.tofhir.server.util.CsvUtil.CsvHeader
import io.tofhir.server.util.{CsvUtil, FileOperations}
import org.json4s.jackson.Serialization.writePretty

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.util.CsvUtil.CsvHeader

import scala.concurrent.Future

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import akka.stream.scaladsl.Source
import akka.util.ByteString
import com.typesafe.scalalogging.LazyLogging
import io.tofhir.server.repository.mappingContext.IMappingContextRepository
import io.tofhir.server.util.CsvUtil.CsvHeader

import scala.concurrent.Future

Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import akka.util.ByteString
import com.typesafe.scalalogging.LazyLogging
import io.tofhir.server.model.TerminologySystem.TerminologyCodeSystem
import io.tofhir.server.repository.terminology.codesystem.ICodeSystemRepository
import io.tofhir.server.util.CsvUtil.CsvHeader

import scala.concurrent.Future

Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import akka.util.ByteString
import com.typesafe.scalalogging.LazyLogging
import io.tofhir.server.model.TerminologySystem.TerminologyConceptMap
import io.tofhir.server.repository.terminology.conceptmap.IConceptMapRepository

import io.tofhir.server.util.CsvUtil.CsvHeader
import scala.concurrent.Future

class ConceptMapService(conceptMapRepository: IConceptMapRepository) extends LazyLogging {
Expand Down Expand Up @@ -67,7 +67,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)
}

Expand Down
39 changes: 28 additions & 11 deletions tofhir-server/src/main/scala/io/tofhir/server/util/CsvUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ 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 (<column_name>)
* - order of the columns are preserved according to the newHHeaders
* @param file CSV file
* @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()

Expand All @@ -79,34 +80,43 @@ object CsvUtil {
}
}


// here an example existingContent looks like:
// [
// [ "header1" -> "value1", "header2" -> "value2" ],
// [ "header1" -> "value3", "header2" -> "value4" ]
// [ "header1" -> "value1", "header2" -> "value2", "header3" -> "value3" ],
// [ "header1" -> "value4", "header2" -> "value5", "header3" -> "value6" ]
// ]
//
// an example newHeaders looks like:
// [ "header2", "header3" ]
// [ "header2", "headerChanged", "header4" ]
// "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
var 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 -> <header3>)
header -> row.find(_._1 == header).map(_._2).getOrElse(s"<$header>")
// If name (field) is different than the previously saved name, update the header of tuples
val key = if (header.field != header.savedName) header.field else header.savedName
// if the header already exists or header name is changed, use its value (header2 -> value2, headerChanged -> value3)
// otherwise, use a default value (header4 -> <header4>)
key -> row.find(_._1 == header.savedName).map(_._2).getOrElse(s"<${header.field}>")
}
}

// here an example updatedContent looks like:
// [
// [ "header2" -> "value2", "header3" -> "<header3>" ],
// [ "header2" -> "value4", "header3" -> "<header3>" ]
// [ "header2" -> "value2", "headerChanged" -> "value3" ,"header4" -> "<header4>" ],
// [ "header2" -> "value5", "headerChanged" -> "value6" ,"header4" -> "<header4>" ]
// ]

// Extract the new names from CsvHeader array
val newHeaderNames = newHeaders.map(_.field)
// 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)))
Expand Down Expand Up @@ -206,4 +216,11 @@ object CsvUtil {

}

/**
* Represents a CSV header with a new name and a previously saved name.
* @param field The current name of the header.
* @param savedName 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(field: String, savedName: String)
}

0 comments on commit 936d487

Please sign in to comment.