Skip to content

Commit

Permalink
feat(lapis2): stream data from SILO
Browse files Browse the repository at this point in the history
Refs: #744
  • Loading branch information
fengelniederhammer committed Apr 22, 2024
1 parent f6a6184 commit d8ade36
Show file tree
Hide file tree
Showing 34 changed files with 563 additions and 423 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
import org.springframework.stereotype.Component
import org.springframework.web.context.annotation.RequestScope
import org.springframework.web.filter.OncePerRequestFilter
import java.io.IOException
import java.io.OutputStream
import java.nio.charset.Charset
import java.util.Enumeration
Expand Down Expand Up @@ -154,8 +155,12 @@ class CompressionFilter(val objectMapper: ObjectMapper, val requestCompression:
maybeCompressingResponse,
)

maybeCompressingResponse.outputStream.flush()
maybeCompressingResponse.outputStream.close()
try {
maybeCompressingResponse.outputStream.flush()
maybeCompressingResponse.outputStream.close()
} catch (e: IOException) {
log.debug { "Failed to flush and close the compressing output stream: ${e.message}" }
}
}

private fun getValidatedCompressionProperty(reReadableRequest: CachedBodyHttpServletRequest): Compression? {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.genspectrum.lapis.controller

import org.genspectrum.lapis.controller.LapisMediaType.TEXT_CSV_WITHOUT_HEADERS
import org.genspectrum.lapis.controller.LapisMediaType.TEXT_CSV_WITHOUT_HEADERS_VALUE

const val DETAILS_ENDPOINT_DESCRIPTION = """Returns the specified metadata fields of sequences matching the filter."""
const val AGGREGATED_ENDPOINT_DESCRIPTION =
Expand Down Expand Up @@ -53,7 +53,7 @@ const val OFFSET_DESCRIPTION =
This is useful for pagination in combination with \"limit\"."""
const val FORMAT_DESCRIPTION = """The data format of the response.
Alternatively, the data format can be specified by setting the \"Accept\"-header.
You can include the parameter to return the CSV/TSV without headers: "$TEXT_CSV_WITHOUT_HEADERS".
You can include the parameter to return the CSV/TSV without headers: "$TEXT_CSV_WITHOUT_HEADERS_VALUE".
When both are specified, the request parameter takes precedence over the header."""

private const val MAYBE_DESCRIPTION = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,42 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVPrinter
import org.springframework.stereotype.Component
import java.io.StringWriter
import java.util.stream.Stream

interface CsvRecord {
@JsonIgnore
fun getValuesList(): List<String?>

@JsonIgnore
fun getHeader(): Array<String>
fun getHeader(): Iterable<String>
}

@Component
class CsvWriter {
fun write(
headers: Array<String>?,
data: List<CsvRecord>,
appendable: Appendable,
includeHeaders: Boolean,
data: Stream<out CsvRecord>,
delimiter: Delimiter,
): String {
val stringWriter = StringWriter()
) {
var shouldWriteHeaders = includeHeaders

CSVPrinter(
stringWriter,
appendable,
CSVFormat.DEFAULT.builder()
.setRecordSeparator("\n")
.setDelimiter(delimiter.value)
.setNullString("")
.also {
when {
headers != null -> it.setHeader(*headers)
}
}
.build(),
).use {
for (datum in data) {
if (shouldWriteHeaders) {
it.printRecord(datum.getHeader())
shouldWriteHeaders = false
}
it.printRecord(datum.getValuesList())
}
}
return stringWriter.toString().trimEnd('\n')
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import jakarta.servlet.http.HttpServletResponse
import mu.KotlinLogging
import org.genspectrum.lapis.util.CachedBodyHttpServletRequest
import org.genspectrum.lapis.util.HeaderModifyingRequestWrapper
import org.genspectrum.lapis.util.ResponseWithContentType
import org.springframework.core.annotation.Order
import org.springframework.http.HttpHeaders.ACCEPT
import org.springframework.http.InvalidMediaTypeException
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
Expand Down Expand Up @@ -44,37 +42,16 @@ class DataFormatParameterFilter(val objectMapper: ObjectMapper) : OncePerRequest

filterChain.doFilter(
requestWithModifiedAcceptHeader,
when (isCsvOrTsvWithoutHeaders(requestWithModifiedAcceptHeader)) {
true -> ResponseWithContentType(response, MediaType.TEXT_PLAIN_VALUE)
false -> response
},
response,
)
}

private fun findAcceptHeaderOverwriteValue(reReadableRequest: CachedBodyHttpServletRequest) =
when (reReadableRequest.getStringField(FORMAT_PROPERTY)?.uppercase()) {
DataFormat.CSV -> LapisMediaType.TEXT_CSV
DataFormat.CSV_WITHOUT_HEADERS -> LapisMediaType.TEXT_CSV_WITHOUT_HEADERS
DataFormat.TSV -> LapisMediaType.TEXT_TSV
DataFormat.CSV -> LapisMediaType.TEXT_CSV_VALUE
DataFormat.CSV_WITHOUT_HEADERS -> LapisMediaType.TEXT_CSV_WITHOUT_HEADERS_VALUE
DataFormat.TSV -> LapisMediaType.TEXT_TSV_VALUE
DataFormat.JSON -> MediaType.APPLICATION_JSON_VALUE
else -> null
}

private fun isCsvOrTsvWithoutHeaders(requestWithModifiedAcceptHeader: HeaderModifyingRequestWrapper): Boolean {
val acceptHeader = requestWithModifiedAcceptHeader.getHeader(ACCEPT) ?: return false

val acceptMediaType = try {
MediaType.parseMediaType(acceptHeader)
} catch (e: InvalidMediaTypeException) {
log.info { "failed to parse accept header: " + e.message }
return false
}

if (acceptMediaType.parameters[HEADERS_ACCEPT_HEADER_PARAMETER] == "false") {
log.info { "Setting response content type to plain text due to 'headers=false'" }
return true
}

return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.genspectrum.lapis.controller.LapisMediaType.TEXT_CSV
import org.genspectrum.lapis.controller.LapisMediaType.TEXT_TSV
import org.genspectrum.lapis.controller.LapisMediaType.TEXT_CSV_VALUE
import org.genspectrum.lapis.controller.LapisMediaType.TEXT_TSV_VALUE
import org.genspectrum.lapis.util.CachedBodyHttpServletRequest
import org.springframework.core.annotation.Order
import org.springframework.http.HttpHeaders.ACCEPT
Expand Down Expand Up @@ -45,8 +45,8 @@ class DownloadAsFileFilter(
}

val fileEnding = when (request.getHeader(ACCEPT)) {
TEXT_CSV -> "csv"
TEXT_TSV -> "tsv"
TEXT_CSV_VALUE -> "csv"
TEXT_TSV_VALUE -> "tsv"
else -> when (matchingRoute?.servesFasta) {
true -> "fasta"
else -> "json"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.genspectrum.lapis.controller

import org.springframework.http.MediaType

object LapisHeaders {
const val REQUEST_ID = "X-Request-ID"
const val LAPIS_DATA_VERSION = "Lapis-Data-Version"
}

object LapisMediaType {
const val TEXT_X_FASTA = "text/x-fasta"
const val TEXT_CSV = "text/csv"
const val TEXT_CSV_WITHOUT_HEADERS = "text/csv;$HEADERS_ACCEPT_HEADER_PARAMETER=false"
const val TEXT_TSV = "text/tab-separated-values"
const val APPLICATION_YAML = "application/yaml"
const val TEXT_X_FASTA_VALUE = "text/x-fasta"
val TEXT_X_FASTA = MediaType.parseMediaType(TEXT_X_FASTA_VALUE)
const val TEXT_CSV_VALUE = "text/csv"
const val TEXT_CSV_WITHOUT_HEADERS_VALUE = "text/csv;$HEADERS_ACCEPT_HEADER_PARAMETER=false"
const val TEXT_TSV_VALUE = "text/tab-separated-values"
const val APPLICATION_YAML_VALUE = "application/yaml"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package org.genspectrum.lapis.controller
import io.swagger.v3.oas.annotations.Operation
import org.genspectrum.lapis.config.DatabaseConfig
import org.genspectrum.lapis.config.ReferenceGenome
import org.genspectrum.lapis.controller.LapisMediaType.APPLICATION_YAML
import org.genspectrum.lapis.controller.LapisMediaType.APPLICATION_YAML_VALUE
import org.genspectrum.lapis.model.SiloQueryModel
import org.genspectrum.lapis.request.LapisInfo
import org.springframework.http.MediaType
Expand All @@ -29,7 +29,7 @@ class InfoController(
return LapisInfo(siloInfo.dataVersion)
}

@GetMapping(DATABASE_CONFIG_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE, APPLICATION_YAML])
@GetMapping(DATABASE_CONFIG_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE, APPLICATION_YAML_VALUE])
@Operation(description = DATABASE_CONFIG_ENDPOINT_DESCRIPTION)
fun getDatabaseConfigAsJson(): DatabaseConfig = databaseConfig

Expand Down
Loading

0 comments on commit d8ade36

Please sign in to comment.