From 2ca50f113aeed5d7862d8219de0c126b1e427a28 Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:02:14 +0100 Subject: [PATCH 1/2] add support for cache control --- .../api/v3/dataSources/APIDataStore.kt | 2 + .../api/v3/dataSources/APIDataStoreImpl.kt | 4 ++ .../persitence/mongo/UpdatedInfo.kt | 16 ++++- .../adoptium/api/v3/CacheControlService.kt | 51 ++++++++++++++++ .../routes/info/AvailableReleasesResource.kt | 14 +++++ .../api/v3/routes/info/ReleaseListResource.kt | 20 +++---- .../api/v3/routes/packages/BinaryResource.kt | 4 +- .../v3/routes/packages/ChecksumResource.kt | 2 +- .../v3/routes/packages/InstallerResource.kt | 4 +- .../v3/routes/packages/SignatureResource.kt | 2 +- .../net/adoptium/api/ApiDataStoreStub.kt | 10 ++++ .../AssetsResourceFeatureReleasePathTest.kt | 58 +++++++++++++++++++ .../api/AssetsResourceVersionPathTest.kt | 2 +- .../net/adoptium/api/APIDataStoreTest.kt | 7 ++- 14 files changed, 175 insertions(+), 21 deletions(-) create mode 100644 adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/CacheControlService.kt diff --git a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/APIDataStore.kt b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/APIDataStore.kt index b1a0f0940..596ffacbb 100644 --- a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/APIDataStore.kt +++ b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/APIDataStore.kt @@ -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 { @@ -9,4 +10,5 @@ interface APIDataStore { fun setAdoptRepos(binaryRepos: AdoptRepos) fun getReleaseInfo(): ReleaseInfo fun loadDataFromDb(forceUpdate: Boolean): AdoptRepos + fun getUpdateInfo(): UpdatedInfo } diff --git a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/APIDataStoreImpl.kt b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/APIDataStoreImpl.kt index f4b7763ca..2fcbe6335 100644 --- a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/APIDataStoreImpl.kt +++ b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/APIDataStoreImpl.kt @@ -188,6 +188,10 @@ open class APIDataStoreImpl : APIDataStore { } + override fun getUpdateInfo(): UpdatedInfo { + return updatedAt + } + // open for override fun getAdoptRepos(): AdoptRepos { return binaryRepos diff --git a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/UpdatedInfo.kt b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/UpdatedInfo.kt index 98019ef80..877bcb7a8 100644 --- a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/UpdatedInfo.kt +++ b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/UpdatedInfo.kt @@ -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" } diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/CacheControlService.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/CacheControlService.kt new file mode 100644 index 000000000..19ce05e3a --- /dev/null +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/CacheControlService.kt @@ -0,0 +1,51 @@ +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 + + 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 + + responseContext?.headers?.add("ETag", apiDataStore.getUpdateInfo().hexChecksum) + responseContext?.headers?.add("Last-Modified", apiDataStore.getUpdateInfo().lastModifiedFormatted) + responseContext?.headers?.add("Cache-Control", CacheControlDelegate.INSTANCE.toString(ecc)) + } + +} diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/info/AvailableReleasesResource.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/info/AvailableReleasesResource.kt index b3171fb21..71b360b10 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/info/AvailableReleasesResource.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/info/AvailableReleasesResource.kt @@ -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") @@ -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() diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/info/ReleaseListResource.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/info/ReleaseListResource.kt index 70a273a66..4f6ad521c 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/info/ReleaseListResource.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/info/ReleaseListResource.kt @@ -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 @@ -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") diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/BinaryResource.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/BinaryResource.kt index addfdeb85..4de114c3a 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/BinaryResource.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/BinaryResource.kt @@ -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", @@ -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", diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/ChecksumResource.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/ChecksumResource.kt index 58756099a..4469c0002 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/ChecksumResource.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/ChecksumResource.kt @@ -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", diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/InstallerResource.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/InstallerResource.kt index cd1b81255..7e48b89c7 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/InstallerResource.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/InstallerResource.kt @@ -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", @@ -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", diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/SignatureResource.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/SignatureResource.kt index d29467fdb..5424424a2 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/SignatureResource.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/routes/packages/SignatureResource.kt @@ -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", diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/ApiDataStoreStub.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/ApiDataStoreStub.kt index acd80ee6c..040005870 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/ApiDataStoreStub.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/ApiDataStoreStub.kt @@ -5,7 +5,9 @@ import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.inject.Alternative import net.adoptium.api.v3.dataSources.APIDataStore import net.adoptium.api.v3.dataSources.models.AdoptRepos +import net.adoptium.api.v3.dataSources.persitence.mongo.UpdatedInfo import net.adoptium.api.v3.models.ReleaseInfo +import java.time.ZonedDateTime @Priority(1) @Alternative @@ -56,4 +58,12 @@ open class ApiDataStoreStub : APIDataStore { // nop return adoptRepo } + + override fun getUpdateInfo(): UpdatedInfo { + return UpdatedInfo( + ZonedDateTime.now(), + "1234567890", + 123 + ) + } } diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/AssetsResourceFeatureReleasePathTest.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/AssetsResourceFeatureReleasePathTest.kt index 262c68b0b..44fd22a19 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/AssetsResourceFeatureReleasePathTest.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/AssetsResourceFeatureReleasePathTest.kt @@ -22,6 +22,8 @@ import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestFactory import org.junit.jupiter.api.extension.ExtendWith +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter import java.util.stream.Stream @QuarkusTest @@ -238,4 +240,60 @@ class AssetsResourceFeatureReleasePathTest : AssetsPathTest() { } .stream() } + + @Test + fun `cache control headers are present`() { + RestAssured.given() + .`when`() + .get("/v3/assets/feature_releases/8/ga") + .then() + .statusCode(200) + .assertThat() + .header("Cache-Control", Matchers.equalTo("public, no-transform")) + .header("ETag", Matchers.equalTo("d76df8e7aefcf7")) + .header("Last-Modified", Matchers.notNullValue()) + } + + @Test + fun `if none match applied`() { + RestAssured.given() + .`when`() + .header("If-None-Match", "d76df8e7aefcf7") + .get("/v3/assets/feature_releases/8/ga") + .then() + .statusCode(304) + } + + @Test + fun `etag applied match applied`() { + RestAssured.given() + .`when`() + .header("If-Match", "d76df8e7aefcf7") + .get("/v3/assets/feature_releases/8/ga") + .then() + .statusCode(200) + } + + @Test + fun `modified match applied`() { + RestAssured.given() + .`when`() + .header("If-Modified-Since", ZonedDateTime.now().plusDays(1).format(DateTimeFormatter.RFC_1123_DATE_TIME)) + .get("/v3/assets/feature_releases/8/ga") + .then() + .statusCode(304) + } + + @Test + fun `modified match applied2`() { + RestAssured.given() + .`when`() + .header("If-Modified-Since", ZonedDateTime.now().minusYears(100) + .format(DateTimeFormatter.RFC_1123_DATE_TIME) + ) + .get("/v3/assets/feature_releases/8/ga") + .then() + .statusCode(200) + } + } diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/AssetsResourceVersionPathTest.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/AssetsResourceVersionPathTest.kt index 16d2c490d..5defa9ba1 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/AssetsResourceVersionPathTest.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/test/kotlin/net/adoptium/api/AssetsResourceVersionPathTest.kt @@ -96,7 +96,6 @@ class AssetsResourceVersionPathTest : AssetsPathTest() { .stream() } - @TestFactory fun `semver does not match out of range`(): Stream { return listOf( @@ -184,4 +183,5 @@ class AssetsResourceVersionPathTest : AssetsPathTest() { element == OperatingSystem.`alpine-linux` || (element == JvmImpl.dragonwell).xor(versionRange.equals(JAVA8_212) || versionRange.equals(JAVA11)) } + } diff --git a/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/APIDataStoreTest.kt b/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/APIDataStoreTest.kt index 0341f1097..32859f170 100644 --- a/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/APIDataStoreTest.kt +++ b/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/APIDataStoreTest.kt @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.skyscreamer.jsonassert.JSONAssert import org.slf4j.LoggerFactory +import java.util.* class APIDataStoreTest : MongoTest() { @@ -63,15 +64,15 @@ class APIDataStoreTest : MongoTest() { @Test fun `updated at is set`(apiPersistence: ApiPersistence) { runBlocking { - apiPersistence.updateAllRepos(BaseTest.adoptRepos, "") + apiPersistence.updateAllRepos(BaseTest.adoptRepos, Base64.getEncoder().encodeToString("1234".toByteArray())) val time = TimeSource.now() delay(1000) - apiPersistence.updateAllRepos(BaseTest.adoptRepos, "a-checksum") + apiPersistence.updateAllRepos(BaseTest.adoptRepos, Base64.getEncoder().encodeToString("a-checksum".toByteArray())) val updatedTime = apiPersistence.getUpdatedAt() assertTrue(updatedTime.time.isAfter(time)) - assertEquals("a-checksum", updatedTime.checksum) + assertEquals(Base64.getEncoder().encodeToString("a-checksum".toByteArray()), updatedTime.checksum) } } From cdf4948585e51a3cda560d4174bd883a5af1b169 Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:04:12 +0100 Subject: [PATCH 2/2] Fix date serialization --- .../persitence/mongo/UpdatedInfo.kt | 12 ++--- .../persitence/mongo/codecs/DateCodecs.kt | 53 +++++++++++++++++++ .../mongo/codecs/JacksonCodecProvider.kt | 9 +++- .../mongo/codecs/ZonedDateTimeCodecs.kt | 50 +++++++++++++++++ .../mongo/codecs/ZonedDateTimeDeserializer.kt | 32 ----------- .../adoptium/api/v3/CacheControlService.kt | 9 ++++ .../net/adoptium/api/DateTimeMigrationTest.kt | 23 ++++++++ .../test/kotlin/net/adoptium/api/MongoTest.kt | 5 +- 8 files changed, 151 insertions(+), 42 deletions(-) create mode 100644 adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/DateCodecs.kt create mode 100644 adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/ZonedDateTimeCodecs.kt delete mode 100644 adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/ZonedDateTimeDeserializer.kt diff --git a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/UpdatedInfo.kt b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/UpdatedInfo.kt index 877bcb7a8..0d8d63683 100644 --- a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/UpdatedInfo.kt +++ b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/UpdatedInfo.kt @@ -10,12 +10,12 @@ 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)) { + 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)) { override fun toString(): String { return "$time $checksum $hashCode" diff --git a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/DateCodecs.kt b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/DateCodecs.kt new file mode 100644 index 000000000..ece63ae92 --- /dev/null +++ b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/DateCodecs.kt @@ -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() { + 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() { + override fun deserialize(jsonParser: JsonParser?, context: DeserializationContext?): Date { + when (val value = jsonParser?.readValueAsTree()) { + 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") + } + + } + +} diff --git a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/JacksonCodecProvider.kt b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/JacksonCodecProvider.kt index 93df8c708..31de4700f 100644 --- a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/JacksonCodecProvider.kt +++ b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/JacksonCodecProvider.kt @@ -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 { @@ -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()) } }) } @@ -38,7 +42,8 @@ class JacksonCodecProvider : CodecProvider { } } -class JacksonCodec(private val objectMapper: ObjectMapper, private val registry: CodecRegistry, val type: Class) : Codec { +class JacksonCodec(private val objectMapper: ObjectMapper, private val registry: CodecRegistry, val type: Class) : + Codec { private var rawBsonDocumentCodec: Codec = registry.get(RawBsonDocument::class.java) diff --git a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/ZonedDateTimeCodecs.kt b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/ZonedDateTimeCodecs.kt new file mode 100644 index 000000000..875bc6eca --- /dev/null +++ b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/ZonedDateTimeCodecs.kt @@ -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() { + override fun deserialize(jsonParser: JsonParser?, context: DeserializationContext?): ZonedDateTime { + when (val value = jsonParser?.readValueAsTree()) { + 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() { + override fun serialize(p0: ZonedDateTime?, p1: JsonGenerator?, p2: SerializerProvider?) { + p1?.writeStartObject(); + p1?.writeStringField("\$date", p0?.format(DateTimeFormatter.ISO_INSTANT)); + p1?.writeEndObject(); + } + + } +} diff --git a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/ZonedDateTimeDeserializer.kt b/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/ZonedDateTimeDeserializer.kt deleted file mode 100644 index 0fe88b093..000000000 --- a/adoptium-api-v3-persistence/src/main/kotlin/net/adoptium/api/v3/dataSources/persitence/mongo/codecs/ZonedDateTimeDeserializer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package net.adoptium.api.v3.dataSources.persitence.mongo.codecs - -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.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.ZonedDateTime - -class ZonedDateTimeDeserializer : JsonDeserializer() { - override fun deserialize(jsonParser: JsonParser?, context: DeserializationContext?): ZonedDateTime { - when (val value = jsonParser?.readValueAsTree()) { - is ObjectNode -> { - return ZonedDateTime.parse(value.get("\$date").asText()) - } - - is TextNode -> { - return ZonedDateTime.parse(value.asText()) - } - - is LongNode -> { - return ZonedDateTime.ofInstant(java.time.Instant.ofEpochMilli(value.asLong()), TimeSource.ZONE) - } - } - - throw IllegalArgumentException("Could not parse ZonedDateTime") - } - -} diff --git a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/CacheControlService.kt b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/CacheControlService.kt index 19ce05e3a..0532cf011 100644 --- a/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/CacheControlService.kt +++ b/adoptium-frontend-parent/adoptium-api-v3-frontend/src/main/kotlin/net/adoptium/api/v3/CacheControlService.kt @@ -26,6 +26,10 @@ class CacheControlService @Inject constructor(private var apiDataStore: APIDataS val etag = apiDataStore.getUpdateInfo().hexChecksum val lastModified = apiDataStore.getUpdateInfo().lastModified + if (lastModified == null || etag == null) { + return + } + val builder = requestContext .request @@ -43,6 +47,11 @@ class CacheControlService @Inject constructor(private var apiDataStore: APIDataS 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)) diff --git a/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/DateTimeMigrationTest.kt b/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/DateTimeMigrationTest.kt index dbfcfca58..567d0598f 100644 --- a/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/DateTimeMigrationTest.kt +++ b/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/DateTimeMigrationTest.kt @@ -6,6 +6,7 @@ import net.adoptium.api.v3.JsonMapper import net.adoptium.api.v3.TimeSource import net.adoptium.api.v3.dataSources.persitence.mongo.MongoClient import net.adoptium.api.v3.models.DateTime +import net.adoptium.api.v3.models.GitHubDownloadStatsDbEntry import org.bson.Document import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -59,6 +60,28 @@ class DateTimeMigrationTest : MongoTest() { } } + @Test + fun `github stats`(mongoClient: MongoClient) { + runBlocking { + val collectionName = UUID.randomUUID().toString() + val client1 = mongoClient.getDatabase().getCollection(collectionName) + + client1.insertOne( + GitHubDownloadStatsDbEntry( + TimeSource.now(), + 1, + mapOf(), + 1 + ) + ) + + client1.find().firstOrNull()?.let { + assertEquals(1, it.downloads) + } + } + + } + @Test fun `writes datetime as string`() { val hzdt = HasDateTime(DateTime(ZonedDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneOffset.UTC))) diff --git a/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/MongoTest.kt b/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/MongoTest.kt index 7c1a6d854..794c536b8 100644 --- a/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/MongoTest.kt +++ b/adoptium-updater-parent/adoptium-api-v3-updater/src/test/kotlin/net/adoptium/api/MongoTest.kt @@ -1,5 +1,6 @@ package net.adoptium.api +import de.flapdoodle.embed.mongo.config.ImmutableNet import de.flapdoodle.embed.mongo.config.Net import de.flapdoodle.embed.mongo.distribution.Version import de.flapdoodle.embed.mongo.transitions.Mongod @@ -34,9 +35,9 @@ abstract class MongoTest { fun startFongo() { val bindIp = "localhost" - val port = de.flapdoodle.net.Net.freeServerPort() + val net = ImmutableNet.defaults() - val net = Net.builder().bindIp(bindIp).port(port).isIpv6(false).build() + val port = net.port val mongodbTestConnectionString = "mongodb://$bindIp:$port"