Skip to content

Commit e30efa1

Browse files
authored
All fetch endpoints now have conditional headers (#176)
Signed-off-by: Arnau Mora Gras <[email protected]>
1 parent f5cf508 commit e30efa1

15 files changed

+179
-5
lines changed

src/main/kotlin/database/entity/Path.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ class Path(id: EntityID<Int>): BaseEntity(id), ResponseData {
141141
result = 31 * result + (builder?.hashCode() ?: 0)
142142
result = 31 * result + (reBuilder?.hashCode() ?: 0)
143143
result = 31 * result + (_images?.hashCode() ?: 0)
144-
result = 31 * result + sector.hashCode()
144+
result = 31 * result + sector.id.value.hashCode()
145145
return result
146146
}
147147

src/main/kotlin/database/entity/Sector.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class Sector(id: EntityID<Int>): BaseEntity(id), ResponseData {
8888
result = 31 * result + (gpx?.hashCode() ?: 0)
8989
result = 31 * result + (point?.hashCode() ?: 0)
9090
result = 31 * result + (weight.hashCode())
91-
result = 31 * result + zone.hashCode()
91+
result = 31 * result + zone.id.value.hashCode()
9292
return result
9393
}
9494

src/main/kotlin/database/entity/Zone.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class Zone(id: EntityID<Int>): DataEntity(id), ResponseData {
100100
result = 31 * result + webUrl.toString().hashCode()
101101
result = 31 * result + (point?.hashCode() ?: 0)
102102
result = 31 * result + points.hashCode()
103-
result = 31 * result + area.hashCode()
103+
result = 31 * result + area.id.value.hashCode()
104104
return result
105105
}
106106

src/main/kotlin/server/endpoints/query/AreaEndpoint.kt

+7
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package server.endpoints.query
22

33
import ServerDatabase
44
import database.entity.Area
5+
import io.ktor.http.HttpHeaders
56
import io.ktor.server.plugins.ParameterConversionException
7+
import io.ktor.server.response.header
68
import io.ktor.server.routing.RoutingContext
79
import io.ktor.server.util.getValue
810
import server.endpoints.EndpointBase
911
import server.error.Errors
12+
import server.response.ResourceId
13+
import server.response.ResourceType
1014
import server.response.respondFailure
1115
import server.response.respondSuccess
1216

@@ -22,6 +26,9 @@ object AreaEndpoint : EndpointBase("/area/{areaId}") {
2226
val area = ServerDatabase.instance.query { Area.findById(areaId) }
2327
?: return respondFailure(Errors.ObjectNotFound)
2428

29+
call.response.header(HttpHeaders.ResourceType, "Area")
30+
call.response.header(HttpHeaders.ResourceId, areaId.toString())
31+
2532
respondSuccess(data = area)
2633
}
2734
}

src/main/kotlin/server/endpoints/query/PathEndpoint.kt

+6
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import database.entity.Path
55
import io.ktor.http.HttpHeaders
66
import io.ktor.server.plugins.ParameterConversionException
77
import io.ktor.server.request.header
8+
import io.ktor.server.response.header
89
import io.ktor.server.routing.RoutingContext
910
import io.ktor.server.util.getValue
1011
import localization.Localization
1112
import server.endpoints.EndpointBase
1213
import server.error.Errors
14+
import server.response.ResourceId
15+
import server.response.ResourceType
1316
import server.response.respondFailure
1417
import server.response.respondSuccess
1518

@@ -37,6 +40,9 @@ object PathEndpoint : EndpointBase("/path/{pathId}") {
3740
}
3841
}
3942

43+
call.response.header(HttpHeaders.ResourceType, "Path")
44+
call.response.header(HttpHeaders.ResourceId, pathId.toString())
45+
4046
respondSuccess(path)
4147
}
4248
}

src/main/kotlin/server/endpoints/query/SectorEndpoint.kt

+7
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package server.endpoints.query
22

33
import ServerDatabase
44
import database.entity.Sector
5+
import io.ktor.http.HttpHeaders
56
import io.ktor.server.plugins.ParameterConversionException
7+
import io.ktor.server.response.header
68
import io.ktor.server.routing.RoutingContext
79
import io.ktor.server.util.getValue
810
import server.endpoints.EndpointBase
911
import server.error.Errors
12+
import server.response.ResourceId
13+
import server.response.ResourceType
1014
import server.response.respondFailure
1115
import server.response.respondSuccess
1216

@@ -22,6 +26,9 @@ object SectorEndpoint : EndpointBase("/sector/{sectorId}") {
2226
val sector = ServerDatabase.instance.query { Sector.findById(sectorId) }
2327
?: return respondFailure(Errors.ObjectNotFound)
2428

29+
call.response.header(HttpHeaders.ResourceType, "Sector")
30+
call.response.header(HttpHeaders.ResourceId, sectorId.toString())
31+
2532
respondSuccess(sector)
2633
}
2734
}

src/main/kotlin/server/endpoints/query/TreeEndpoint.kt

+28
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,41 @@ import database.entity.Area
55
import database.entity.Path
66
import database.entity.Sector
77
import database.entity.Zone
8+
import database.table.Areas
9+
import database.table.Paths
10+
import database.table.Sectors
11+
import database.table.Zones
12+
import io.ktor.http.HttpHeaders
13+
import io.ktor.server.response.header
814
import io.ktor.server.routing.RoutingContext
15+
import org.jetbrains.exposed.sql.SortOrder
916
import server.endpoints.EndpointBase
1017
import server.response.query.TreeResponseData
1118
import server.response.respondSuccess
1219

1320
object TreeEndpoint : EndpointBase("/tree") {
21+
@OptIn(ExperimentalStdlibApi::class)
1422
override suspend fun RoutingContext.endpoint() {
23+
val lastUpdatedArea = ServerDatabase {
24+
Area.all().orderBy(Areas.timestamp to SortOrder.DESC).limit(1).firstOrNull()
25+
}
26+
val lastUpdatedZone = ServerDatabase {
27+
Zone.all().orderBy(Zones.timestamp to SortOrder.DESC).limit(1).firstOrNull()
28+
}
29+
val lastUpdatedSector = ServerDatabase {
30+
Sector.all().orderBy(Sectors.timestamp to SortOrder.DESC).limit(1).firstOrNull()
31+
}
32+
val lastUpdatedPath = ServerDatabase {
33+
Path.all().orderBy(Paths.timestamp to SortOrder.DESC).limit(1).firstOrNull()
34+
}
35+
val lastUpdate = listOf(lastUpdatedArea, lastUpdatedZone, lastUpdatedSector, lastUpdatedPath)
36+
.maxByOrNull { it?.timestamp?.epochSecond ?: 0 }
37+
38+
if (lastUpdate != null) {
39+
call.response.header(HttpHeaders.LastModified, lastUpdate.timestamp)
40+
call.response.header(HttpHeaders.ETag, ServerDatabase { lastUpdate.hashCode().toHexString() })
41+
}
42+
1543
val array = ServerDatabase {
1644
val zones = Zone.all().toList()
1745
val sectors = Sector.all().toList()

src/main/kotlin/server/endpoints/query/ZoneEndpoint.kt

+7
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package server.endpoints.query
22

33
import ServerDatabase
44
import database.entity.Zone
5+
import io.ktor.http.HttpHeaders
56
import io.ktor.server.plugins.ParameterConversionException
7+
import io.ktor.server.response.header
68
import io.ktor.server.routing.RoutingContext
79
import io.ktor.server.util.getValue
810
import server.endpoints.EndpointBase
911
import server.error.Errors
12+
import server.response.ResourceId
13+
import server.response.ResourceType
1014
import server.response.respondFailure
1115
import server.response.respondSuccess
1216

@@ -22,6 +26,9 @@ object ZoneEndpoint : EndpointBase("/zone/{zoneId}") {
2226
val zone = ServerDatabase.instance.query { Zone.findById(zoneId) }
2327
?: return respondFailure(Errors.ObjectNotFound)
2428

29+
call.response.header(HttpHeaders.ResourceType, "Zone")
30+
call.response.header(HttpHeaders.ResourceId, zoneId.toString())
31+
2532
respondSuccess(zone)
2633
}
2734
}

src/main/kotlin/server/plugins/ConditionalHeaders.kt

+34-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
package server.plugins
22

3+
import ServerDatabase
4+
import database.entity.Area
5+
import database.entity.Path
6+
import database.entity.Sector
7+
import database.entity.Zone
38
import io.ktor.http.HttpHeaders
49
import io.ktor.http.content.EntityTagVersion
510
import io.ktor.server.http.content.LastModifiedVersion
611
import io.ktor.server.plugins.conditionalheaders.ConditionalHeadersConfig
712
import java.security.MessageDigest
813
import server.response.FileSource
914
import server.response.FileUUID
15+
import server.response.ResourceId
16+
import server.response.ResourceType
1017
import storage.FileType
1118
import storage.HashUtils
1219
import storage.MessageDigestAlgorithm
1320

21+
@OptIn(ExperimentalStdlibApi::class)
1422
fun ConditionalHeadersConfig.configure() {
1523
version { call, outgoingContent ->
16-
val fileUUID = call.response.headers[HttpHeaders.FileUUID]
17-
val fileSource = call.response.headers[HttpHeaders.FileSource]
24+
val headers = call.response.headers
25+
26+
val fileUUID = headers[HttpHeaders.FileUUID]
27+
val fileSource = headers[HttpHeaders.FileSource]
1828
val fileType = FileType.entries.find { it.headerValue == fileSource }
1929
if (fileUUID != null && fileType != null) {
2030
val file = fileType.fetcher(fileUUID)?.takeIf { it.exists() }
@@ -31,6 +41,28 @@ fun ConditionalHeadersConfig.configure() {
3141
}
3242
}
3343

44+
val resourceType = headers[HttpHeaders.ResourceType]
45+
val resourceId = headers[HttpHeaders.ResourceId]?.toIntOrNull()
46+
if (resourceType != null && resourceId != null) {
47+
val resource = ServerDatabase {
48+
when (resourceType) {
49+
"Area" -> Area[resourceId]
50+
"Zone" -> Zone[resourceId]
51+
"Sector" -> Sector[resourceId]
52+
"Path" -> Path[resourceId]
53+
else -> null
54+
}
55+
}
56+
if (resource != null) {
57+
return@version listOfNotNull(
58+
EntityTagVersion(
59+
ServerDatabase { resource.hashCode().toHexString() }
60+
),
61+
LastModifiedVersion(resource.timestamp.toEpochMilli())
62+
)
63+
}
64+
}
65+
3466
emptyList()
3567
}
3668
}

src/main/kotlin/server/response/CustomResponseHeaders.kt

+14
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,17 @@ val HttpHeaders.FileUUID: String get() = "X-File-UUID"
1515
* @see FileType
1616
*/
1717
val HttpHeaders.FileSource: String get() = "X-File-Source"
18+
19+
/**
20+
* A header included in the responses of data requests, containing the type of the data. One of:
21+
* - `Area`
22+
* - `Zone`
23+
* - `Sector`
24+
* - `Path`
25+
*/
26+
val HttpHeaders.ResourceType: String get() = "X-Resource-Type"
27+
28+
/**
29+
* A header included in the responses of data requests, containing the ID of the data.
30+
*/
31+
val HttpHeaders.ResourceId: String get() = "X-Resource-Id"

src/test/kotlin/server/endpoints/query/TestAreaFetchingEndpoint.kt

+14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package server.endpoints.query
22

3+
import ServerDatabase
34
import assertions.assertFailure
45
import assertions.assertIsUUID
56
import assertions.assertSuccess
67
import database.entity.Area
8+
import io.ktor.http.HttpHeaders
9+
import io.ktor.http.etag
10+
import io.ktor.http.lastModified
711
import java.time.Instant
812
import kotlin.test.Test
913
import kotlin.test.assertEquals
@@ -12,10 +16,13 @@ import kotlin.test.assertTrue
1216
import server.DataProvider
1317
import server.base.ApplicationTestBase
1418
import server.error.Errors
19+
import server.response.ResourceId
20+
import server.response.ResourceType
1521
import server.response.files.RequestFileResponseData
1622
import storage.Storage
1723

1824
class TestAreaFetchingEndpoint : ApplicationTestBase() {
25+
@OptIn(ExperimentalStdlibApi::class)
1926
@Test
2027
fun `test getting area`() = test {
2128
val areaId = DataProvider.provideSampleArea(this)
@@ -34,6 +41,13 @@ class TestAreaFetchingEndpoint : ApplicationTestBase() {
3441

3542
image = data.image.toRelativeString(Storage.ImagesDir).substringBeforeLast('.')
3643
}
44+
45+
val area = ServerDatabase { Area[areaId] }
46+
val hashCode = ServerDatabase { area.hashCode().toHexString() }
47+
assertEquals("Area", headers[HttpHeaders.ResourceType])
48+
assertEquals(areaId, headers[HttpHeaders.ResourceId]?.toIntOrNull())
49+
assertEquals(area.timestamp.epochSecond, lastModified()?.toInstant()?.epochSecond)
50+
assertEquals(hashCode, etag()?.trim('"'))
3751
}
3852

3953
assertNotNull(image)

src/test/kotlin/server/endpoints/query/TestPathFetchingEndpoint.kt

+13
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import ServerDatabase
44
import assertions.assertFailure
55
import assertions.assertSuccess
66
import database.entity.Path
7+
import io.ktor.http.HttpHeaders
78
import io.ktor.http.HttpStatusCode
9+
import io.ktor.http.etag
10+
import io.ktor.http.lastModified
811
import java.time.Instant
912
import kotlin.test.Test
1013
import kotlin.test.assertContentEquals
@@ -15,10 +18,13 @@ import kotlin.test.assertTrue
1518
import server.DataProvider
1619
import server.base.ApplicationTestBase
1720
import server.error.Errors
21+
import server.response.ResourceId
22+
import server.response.ResourceType
1823
import server.response.files.RequestFileResponseData
1924
import storage.Storage
2025

2126
class TestPathFetchingEndpoint : ApplicationTestBase() {
27+
@OptIn(ExperimentalStdlibApi::class)
2228
@Test
2329
fun `test getting path`() = test {
2430
val areaId = DataProvider.provideSampleArea(this)
@@ -69,6 +75,13 @@ class TestPathFetchingEndpoint : ApplicationTestBase() {
6975

7076
assertTrue(data.timestamp < Instant.now())
7177
}
78+
79+
val path = ServerDatabase { Path[pathId] }
80+
val hashCode = ServerDatabase { path.hashCode().toHexString() }
81+
assertEquals("Path", headers[HttpHeaders.ResourceType])
82+
assertEquals(pathId, headers[HttpHeaders.ResourceId]?.toIntOrNull())
83+
assertEquals(path.timestamp.epochSecond, lastModified()?.toInstant()?.epochSecond)
84+
assertEquals(hashCode, etag()?.trim('"'))
7285
}
7386
}
7487

src/test/kotlin/server/endpoints/query/TestSectorFetchingEndpoint.kt

+14
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package server.endpoints.query
22

3+
import ServerDatabase
34
import assertions.assertFailure
45
import assertions.assertIsUUID
56
import assertions.assertSuccess
67
import database.entity.Sector
8+
import io.ktor.http.HttpHeaders
79
import io.ktor.http.HttpStatusCode
10+
import io.ktor.http.etag
11+
import io.ktor.http.lastModified
812
import java.time.Instant
913
import kotlin.test.Test
1014
import kotlin.test.assertEquals
@@ -13,10 +17,13 @@ import kotlin.test.assertTrue
1317
import server.DataProvider
1418
import server.base.ApplicationTestBase
1519
import server.error.Errors
20+
import server.response.ResourceId
21+
import server.response.ResourceType
1622
import server.response.files.RequestFileResponseData
1723
import storage.Storage
1824

1925
class TestSectorFetchingEndpoint : ApplicationTestBase() {
26+
@OptIn(ExperimentalStdlibApi::class)
2027
@Test
2128
fun `test getting sector`() = test {
2229
val areaId = DataProvider.provideSampleArea(this)
@@ -47,6 +54,13 @@ class TestSectorFetchingEndpoint : ApplicationTestBase() {
4754
image = data.image.toRelativeString(Storage.ImagesDir).substringBeforeLast('.')
4855
gpx = data.gpx?.toRelativeString(Storage.TracksDir)?.substringBeforeLast('.')
4956
}
57+
58+
val sector = ServerDatabase { Sector[sectorId] }
59+
val hashCode = ServerDatabase { sector.hashCode().toHexString() }
60+
assertEquals("Sector", headers[HttpHeaders.ResourceType])
61+
assertEquals(sectorId, headers[HttpHeaders.ResourceId]?.toIntOrNull())
62+
assertEquals(sector.timestamp.epochSecond, lastModified()?.toInstant()?.epochSecond)
63+
assertEquals(hashCode, etag()?.trim('"'))
5064
}
5165

5266
assertNotNull(image)

0 commit comments

Comments
 (0)