Skip to content

Commit

Permalink
Merge pull request #1207 from johnoliver/cache-validation
Browse files Browse the repository at this point in the history
Add cache validation headers. Fix date time serialization
  • Loading branch information
karianna authored Oct 15, 2024
2 parents ea591b2 + cdf4948 commit 7cbdf77
Show file tree
Hide file tree
Showing 20 changed files with 320 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.adoptium.api.v3.dataSources

import net.adoptium.api.v3.dataSources.models.AdoptRepos
import net.adoptium.api.v3.dataSources.persitence.mongo.UpdatedInfo
import net.adoptium.api.v3.models.ReleaseInfo

interface APIDataStore {
Expand All @@ -9,4 +10,5 @@ interface APIDataStore {
fun setAdoptRepos(binaryRepos: AdoptRepos)
fun getReleaseInfo(): ReleaseInfo
fun loadDataFromDb(forceUpdate: Boolean): AdoptRepos
fun getUpdateInfo(): UpdatedInfo
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ open class APIDataStoreImpl : APIDataStore {

}

override fun getUpdateInfo(): UpdatedInfo {
return updatedAt
}

// open for
override fun getAdoptRepos(): AdoptRepos {
return binaryRepos
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
package net.adoptium.api.v3.dataSources.persitence.mongo

import java.math.BigInteger
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.*

data class UpdatedInfo(
val time: ZonedDateTime,
val checksum: String,
val hashCode: Int,
val hexChecksum: String? = BigInteger(1, Base64.getDecoder().decode(checksum)).toString(16),
val lastModified: Date? = Date.from(time.toInstant()),
val lastModifiedFormatted: String? = lastModified
?.toInstant()
?.atZone(ZoneId.of("GMT"))
?.format(DateTimeFormatter.RFC_1123_DATE_TIME)) {

data class UpdatedInfo(val time: ZonedDateTime, val checksum: String, val hashCode: Int) {
override fun toString(): String {
return "$time $checksum $hashCode"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package net.adoptium.api.v3.dataSources.persitence.mongo.codecs

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.node.LongNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
import net.adoptium.api.v3.TimeSource
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.*

object DateCodecs {
class DateSerializer : JsonSerializer<Date>() {
override fun serialize(p0: Date?, p1: JsonGenerator?, p2: SerializerProvider?) {
p1?.writeStartObject();
p1?.writeStringField(
"\$date",
p0?.toInstant()?.atZone(TimeSource.ZONE)?.format(DateTimeFormatter.ISO_INSTANT)
);
p1?.writeEndObject();
}
}

class DateDeserializer : JsonDeserializer<Date>() {
override fun deserialize(jsonParser: JsonParser?, context: DeserializationContext?): Date {
when (val value = jsonParser?.readValueAsTree<JsonNode>()) {
is ObjectNode -> {
val datetime = DateTimeFormatter.ISO_INSTANT.parse(value.get("\$date").asText())
return Date(Instant.from(datetime).toEpochMilli())
}

is TextNode -> {
val datetime = DateTimeFormatter.ISO_INSTANT.parse(value.asText())
return Date(Instant.from(datetime).toEpochMilli())
}

is LongNode -> {
return Date(value.asLong())
}
}

throw IllegalArgumentException("Could not parse ZonedDateTime")
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.bson.codecs.configuration.CodecRegistry
import java.io.IOException
import java.io.UncheckedIOException
import java.time.ZonedDateTime
import java.util.*

class JacksonCodecProvider : CodecProvider {
companion object {
Expand All @@ -25,7 +26,10 @@ class JacksonCodecProvider : CodecProvider {
.registerModule(JavaTimeModule())
.registerModule(object : SimpleModule() {
init {
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeCodecs.ZonedDateTimeDeserializer())
addSerializer(ZonedDateTime::class.java, ZonedDateTimeCodecs.ZonedDateTimeSerializer())
addDeserializer(Date::class.java, DateCodecs.DateDeserializer())
addSerializer(Date::class.java, DateCodecs.DateSerializer())
}
})
}
Expand All @@ -38,7 +42,8 @@ class JacksonCodecProvider : CodecProvider {
}
}

class JacksonCodec<T>(private val objectMapper: ObjectMapper, private val registry: CodecRegistry, val type: Class<T>) : Codec<T> {
class JacksonCodec<T>(private val objectMapper: ObjectMapper, private val registry: CodecRegistry, val type: Class<T>) :
Codec<T> {

private var rawBsonDocumentCodec: Codec<RawBsonDocument> = registry.get(RawBsonDocument::class.java)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package net.adoptium.api.v3.dataSources.persitence.mongo.codecs

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.node.LongNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
import net.adoptium.api.v3.TimeSource
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter

object ZonedDateTimeCodecs {
class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime>() {
override fun deserialize(jsonParser: JsonParser?, context: DeserializationContext?): ZonedDateTime {
when (val value = jsonParser?.readValueAsTree<JsonNode>()) {
is ObjectNode -> {
val datetime = DateTimeFormatter.ISO_INSTANT.parse(value.get("\$date").asText())
return Instant.from(datetime).atZone(TimeSource.ZONE)
}

is TextNode -> {
val datetime = DateTimeFormatter.ISO_INSTANT.parse(value.asText())
return Instant.from(datetime).atZone(TimeSource.ZONE)
}

is LongNode -> {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(value.asLong()), TimeSource.ZONE)
}
}

throw IllegalArgumentException("Could not parse ZonedDateTime")
}

}

class ZonedDateTimeSerializer : JsonSerializer<ZonedDateTime>() {
override fun serialize(p0: ZonedDateTime?, p1: JsonGenerator?, p2: SerializerProvider?) {
p1?.writeStartObject();
p1?.writeStringField("\$date", p0?.format(DateTimeFormatter.ISO_INSTANT));
p1?.writeEndObject();
}

}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package net.adoptium.api.v3

import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.container.ContainerRequestContext
import jakarta.ws.rs.container.ContainerRequestFilter
import jakarta.ws.rs.container.ContainerResponseContext
import jakarta.ws.rs.core.EntityTag
import jakarta.ws.rs.ext.Provider
import net.adoptium.api.v3.dataSources.APIDataStore
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate
import org.jboss.resteasy.reactive.common.util.ExtendedCacheControl
import org.jboss.resteasy.reactive.server.ServerResponseFilter

@Provider
@ApplicationScoped
class CacheControlService @Inject constructor(private var apiDataStore: APIDataStore) : ContainerRequestFilter {

private val CACHE_CONTROLLED_PATHS = listOf("/v3/info", "/v3/assets")

override fun filter(requestContext: ContainerRequestContext?) {
if (requestContext == null) return

requestContext.uriInfo?.path?.let { path ->
if (CACHE_CONTROLLED_PATHS.any { path.startsWith(it) }) {
val etag = apiDataStore.getUpdateInfo().hexChecksum
val lastModified = apiDataStore.getUpdateInfo().lastModified

if (lastModified == null || etag == null) {
return
}

val builder =
requestContext
.request
.evaluatePreconditions(lastModified, EntityTag(etag))

if (builder != null) {
requestContext.abortWith(builder.build())
}
}
}
}

@ServerResponseFilter
fun responseFilter(responseContext: ContainerResponseContext?) {
val ecc = ExtendedCacheControl();
ecc.isPublic = true

if (apiDataStore.getUpdateInfo().hexChecksum == null ||
apiDataStore.getUpdateInfo().lastModifiedFormatted == null) {
return
}

responseContext?.headers?.add("ETag", apiDataStore.getUpdateInfo().hexChecksum)
responseContext?.headers?.add("Last-Modified", apiDataStore.getUpdateInfo().lastModifiedFormatted)
responseContext?.headers?.add("Cache-Control", CacheControlDelegate.INSTANCE.toString(ecc))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import jakarta.ws.rs.core.MediaType
import net.adoptium.api.v3.dataSources.APIDataStore
import net.adoptium.api.v3.models.ReleaseInfo
import org.eclipse.microprofile.openapi.annotations.Operation
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType
import org.eclipse.microprofile.openapi.annotations.media.Content
import org.eclipse.microprofile.openapi.annotations.media.Schema
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses
import org.eclipse.microprofile.openapi.annotations.tags.Tag

@Tag(name = "Release Info")
Expand All @@ -23,6 +28,15 @@ constructor(

@GET
@Path("/available_releases")
@APIResponses(
value = [
APIResponse(
responseCode = "200",
description = "Available release information",
content = [Content(schema = Schema(type = SchemaType.OBJECT, implementation = ReleaseInfo::class))]
)
]
)
@Operation(summary = "Returns information about available releases", operationId = "getAvailableReleases")
fun get(): ReleaseInfo {
return apiDataStore.getReleaseInfo()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
package net.adoptium.api.v3.routes.info

import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.QueryParam
import jakarta.ws.rs.core.Context
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import jakarta.ws.rs.core.UriInfo
import net.adoptium.api.v3.OpenApiDocs
import net.adoptium.api.v3.Pagination
import net.adoptium.api.v3.Pagination.formPagedResponse
Expand All @@ -26,16 +36,6 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameter
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses
import org.eclipse.microprofile.openapi.annotations.tags.Tag
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.QueryParam
import jakarta.ws.rs.core.Context
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import jakarta.ws.rs.core.UriInfo

@Tag(name = "Release Info")
@Path("/v3/info")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class BinaryResource @Inject constructor(private val packageEndpoint: PackageEnd

@GET
@Path("/version/{release_name}/{os}/{arch}/{image_type}/{jvm_impl}/{heap_size}/{vendor}")
@Produces("application/octet-stream")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Operation(
operationId = "getBinaryByVersion",
summary = "Redirects to the binary that matches your current query",
Expand Down Expand Up @@ -172,7 +172,7 @@ class BinaryResource @Inject constructor(private val packageEndpoint: PackageEnd

@GET
@Path("/latest/{feature_version}/{release_type}/{os}/{arch}/{image_type}/{jvm_impl}/{heap_size}/{vendor}")
@Produces("application/octet-stream")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Operation(
operationId = "getBinary",
summary = "Redirects to the binary that matches your current query",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ChecksumResource @Inject constructor(private val packageEndpoint: PackageE

@GET
@Path("/version/{release_name}/{os}/{arch}/{image_type}/{jvm_impl}/{heap_size}/{vendor}")
@Produces("application/octet-stream")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Operation(
operationId = "getChecksumByVersion",
summary = "Redirects to the checksum of the release that matches your current query",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class InstallerResource @Inject constructor(private val packageEndpoint: Package

@GET
@Path("/version/{release_name}/{os}/{arch}/{image_type}/{jvm_impl}/{heap_size}/{vendor}")
@Produces("application/octet-stream")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Operation(
operationId = "getInstallerByVersion",
summary = "Redirects to the installer that matches your current query",
Expand Down Expand Up @@ -97,7 +97,7 @@ class InstallerResource @Inject constructor(private val packageEndpoint: Package

@GET
@Path("/latest/{feature_version}/{release_type}/{os}/{arch}/{image_type}/{jvm_impl}/{heap_size}/{vendor}")
@Produces("application/octet-stream")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Operation(
operationId = "getInstaller",
summary = "Redirects to the installer that matches your current query",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class SignatureResource @Inject constructor(private val packageEndpoint: Package

@GET
@Path("/version/{release_name}/{os}/{arch}/{image_type}/{jvm_impl}/{heap_size}/{vendor}")
@Produces("application/octet-stream")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Operation(
operationId = "getSignatureByVersion",
summary = "Redirects to the signature of the release that matches your current query",
Expand Down
Loading

0 comments on commit 7cbdf77

Please sign in to comment.