From f2f86d04e90429e363af81cf3ff58d4883595072 Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Thu, 16 Apr 2020 00:34:31 +0100 Subject: [PATCH] add date filter to feature_releases path (#177) --- .../api/v3/filters/BinaryFilter.kt | 19 ++- .../api/v3/routes/AssetsResource.kt | 64 +++++++- .../AssetsResourceFeatureReleasePathTest.kt | 150 +++++++++++------- 3 files changed, 161 insertions(+), 72 deletions(-) diff --git a/adoptopenjdk-api-v3-frontend/src/main/kotlin/net/adoptopenjdk/api/v3/filters/BinaryFilter.kt b/adoptopenjdk-api-v3-frontend/src/main/kotlin/net/adoptopenjdk/api/v3/filters/BinaryFilter.kt index b6a35809..5467ac15 100644 --- a/adoptopenjdk-api-v3-frontend/src/main/kotlin/net/adoptopenjdk/api/v3/filters/BinaryFilter.kt +++ b/adoptopenjdk-api-v3-frontend/src/main/kotlin/net/adoptopenjdk/api/v3/filters/BinaryFilter.kt @@ -7,6 +7,7 @@ import net.adoptopenjdk.api.v3.models.ImageType import net.adoptopenjdk.api.v3.models.JvmImpl import net.adoptopenjdk.api.v3.models.OperatingSystem import net.adoptopenjdk.api.v3.models.Project +import java.time.ZonedDateTime import java.util.function.Predicate class BinaryFilter : Predicate { @@ -17,14 +18,16 @@ class BinaryFilter : Predicate { private val jvmImpl: JvmImpl? private val heapSize: HeapSize? private val project: Project + private var before: ZonedDateTime? constructor( - os: OperatingSystem?, - arch: Architecture?, - imageType: ImageType?, - jvmImpl: JvmImpl?, - heapSize: HeapSize?, - project: Project? + os: OperatingSystem? = null, + arch: Architecture? = null, + imageType: ImageType? = null, + jvmImpl: JvmImpl? = null, + heapSize: HeapSize? = null, + project: Project? = null, + before: ZonedDateTime? = null ) { this.os = os this.arch = arch @@ -32,6 +35,7 @@ class BinaryFilter : Predicate { this.jvmImpl = jvmImpl this.heapSize = heapSize this.project = project ?: Project.jdk + this.before = before } override fun test(binary: Binary): Boolean { @@ -40,6 +44,7 @@ class BinaryFilter : Predicate { (imageType == null || binary.image_type == imageType) && (jvmImpl == null || binary.jvm_impl == jvmImpl) && (heapSize == null || binary.heap_size == heapSize) && - (binary.project == project) + (binary.project == project) && + (before == null || binary.updated_at.isBefore(before)) } } diff --git a/adoptopenjdk-api-v3-frontend/src/main/kotlin/net/adoptopenjdk/api/v3/routes/AssetsResource.kt b/adoptopenjdk-api-v3-frontend/src/main/kotlin/net/adoptopenjdk/api/v3/routes/AssetsResource.kt index d84d51e3..af7ab484 100644 --- a/adoptopenjdk-api-v3-frontend/src/main/kotlin/net/adoptopenjdk/api/v3/routes/AssetsResource.kt +++ b/adoptopenjdk-api-v3-frontend/src/main/kotlin/net/adoptopenjdk/api/v3/routes/AssetsResource.kt @@ -1,6 +1,7 @@ package net.adoptopenjdk.api.v3.routes import net.adoptopenjdk.api.v3.OpenApiDocs +import net.adoptopenjdk.api.v3.TimeSource import net.adoptopenjdk.api.v3.dataSources.APIDataStore import net.adoptopenjdk.api.v3.dataSources.SortOrder import net.adoptopenjdk.api.v3.filters.BinaryFilter @@ -26,6 +27,14 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponses import org.eclipse.microprofile.openapi.annotations.tags.Tag import org.jboss.resteasy.annotations.jaxrs.PathParam import org.jboss.resteasy.annotations.jaxrs.QueryParam +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException +import java.time.temporal.TemporalQuery +import javax.ws.rs.BadRequestException import javax.ws.rs.GET import javax.ws.rs.NotFoundException import javax.ws.rs.Path @@ -92,6 +101,16 @@ class AssetsResource { @QueryParam("project") project: Project?, + @Parameter( + name = "before", + description = "Return binaries whose updated_at is before the given date/time. When a date is given the match is inclusive of the given day.", + schema = Schema(type = SchemaType.STRING), + example = "2020-01-21, 2020-01-21T10:15:30, 20200121, 2020-12-21T10:15:30Z, 2020-12-21+01:00", + required = false + ) + @QueryParam("before") + before: String?, + @Parameter(name = "page_size", description = "Pagination page size", schema = Schema(defaultValue = "10", type = SchemaType.INTEGER), required = false ) @@ -111,8 +130,10 @@ class AssetsResource { ): List { val order = sortOrder ?: SortOrder.DESC + val beforeParsed = parseDate(before) + val releaseFilter = ReleaseFilter(releaseType = release_type, featureVersion = version, vendor = vendor) - val binaryFilter = BinaryFilter(os, arch, image_type, jvm_impl, heap_size, project) + val binaryFilter = BinaryFilter(os, arch, image_type, jvm_impl, heap_size, project, beforeParsed) val repos = APIDataStore.getAdoptRepos().getFeatureRelease(version!!) if (repos == null) { @@ -126,6 +147,43 @@ class AssetsResource { return getPage(pageSize, page, releases) } + private fun parseDate(before: String?): ZonedDateTime? { + return if (before != null) { + try { + val date = LocalDate.parse(before, DateTimeFormatter.ISO_DATE) + return date.plusDays(1).atStartOfDay(TimeSource.ZONE) + } catch (e: DateTimeParseException) { + // NOP + } + + try { + val date = DateTimeFormatter.ISO_DATE_TIME.parseBest(before, + TemporalQuery { p0 -> ZonedDateTime.from(p0) }, + TemporalQuery { p0 -> OffsetDateTime.from(p0) }, + TemporalQuery { p0 -> LocalDateTime.from(p0) } + ) + + return when (date) { + is LocalDateTime -> date.atZone(TimeSource.ZONE) + is OffsetDateTime -> date.atZoneSameInstant(TimeSource.ZONE) + is ZonedDateTime -> date + else -> null + } + } catch (e: DateTimeParseException) { + // NOP + } + + try { + val date = LocalDate.parse(before, DateTimeFormatter.BASIC_ISO_DATE) + return date.plusDays(1).atStartOfDay(TimeSource.ZONE) + } catch (e: DateTimeParseException) { + throw BadRequestException("Cannot parse date") + } + } else { + null + } + } + @GET @Path("/version/{version}") @Operation(summary = "Returns release information about the specified version.", @@ -201,12 +259,10 @@ class AssetsResource { ): List { val order = sortOrder ?: SortOrder.DESC - // Require GA due to version range having no meaning for nightlies - val range = VersionRangeFilter(version) val releaseFilter = ReleaseFilter(releaseType = release_type, vendor = vendor, versionRange = range, lts = lts) - val binaryFilter = BinaryFilter(os, arch, image_type, jvm_impl, heap_size, project) + val binaryFilter = BinaryFilter(os = os, arch = arch, imageType = image_type, jvmImpl = jvm_impl, heapSize = heap_size, project = project) val releases = APIDataStore .getAdoptRepos() diff --git a/adoptopenjdk-api-v3-frontend/src/test/kotlin/net/adoptopenjdk/api/AssetsResourceFeatureReleasePathTest.kt b/adoptopenjdk-api-v3-frontend/src/test/kotlin/net/adoptopenjdk/api/AssetsResourceFeatureReleasePathTest.kt index afb75ab0..43c4bb55 100644 --- a/adoptopenjdk-api-v3-frontend/src/test/kotlin/net/adoptopenjdk/api/AssetsResourceFeatureReleasePathTest.kt +++ b/adoptopenjdk-api-v3-frontend/src/test/kotlin/net/adoptopenjdk/api/AssetsResourceFeatureReleasePathTest.kt @@ -22,98 +22,98 @@ class AssetsResourceFeatureReleasePathTest : AssetsPathTest() { @TestFactory fun noFilter(): Stream { return (8..12) - .flatMap { version -> - ReleaseType.values() - .map { "/v3/assets/feature_releases/$version/$it" } - .map { - DynamicTest.dynamicTest(it) { - RestAssured.given() - .`when`() - .get(it) - .then() - .statusCode(200) - } - } - } - .stream() + .flatMap { version -> + ReleaseType.values() + .map { "/v3/assets/feature_releases/$version/$it" } + .map { + DynamicTest.dynamicTest(it) { + RestAssured.given() + .`when`() + .get(it) + .then() + .statusCode(200) + } + } + } + .stream() } @Test fun badReleaseType() { RestAssured.given() - .`when`() - .get("${getPath()}/8/foo") - .then() - .statusCode(404) + .`when`() + .get("${getPath()}/8/foo") + .then() + .statusCode(404) } @Test fun badVersion() { RestAssured.given() - .`when`() - .get("2/${getPath()}") - .then() - .statusCode(404) + .`when`() + .get("2/${getPath()}") + .then() + .statusCode(404) } @Test fun sortOrderASCIsHonoured() { getReleases(SortOrder.ASC) - .fold(null, { previous: Release?, next: Release -> - if (previous != null) { - if (Releases.VERSION_COMPARATOR.compare(previous, next) > 0) { - fail("${previous.version_data} is before ${next.version_data}") - } + .fold(null, { previous: Release?, next: Release -> + if (previous != null) { + if (Releases.VERSION_COMPARATOR.compare(previous, next) > 0) { + fail("${previous.version_data} is before ${next.version_data}") } - next - }) + } + next + }) } @Test fun sortOrderDESIsHonoured() { getReleases(SortOrder.DESC) - .fold(null, { previous: Release?, next: Release -> - if (previous != null) { - if (Releases.VERSION_COMPARATOR.compare(previous, next) < 0) { - fail("${previous.version_data} is before ${next.version_data}") - } + .fold(null, { previous: Release?, next: Release -> + if (previous != null) { + if (Releases.VERSION_COMPARATOR.compare(previous, next) < 0) { + fail("${previous.version_data} is before ${next.version_data}") } - next - }) + } + next + }) } override fun runFilterTest(filterParamName: String, values: Array): Stream { return ReleaseType.values() - .flatMap { releaseType -> - // test the ltses and 1 non-lts - listOf(8, 11, 12) - .flatMap { version -> - createTest(values, "${getPath()}/$version/$releaseType", filterParamName, { element -> - getExclusions(version, element) - }) - } - } - .stream() + .flatMap { releaseType -> + // test the ltses and 1 non-lts + listOf(8, 11, 12) + .flatMap { version -> + createTest(values, "${getPath()}/$version/$releaseType", filterParamName, { element -> + getExclusions(version, element) + }) + } + } + .stream() } private fun getExclusions(version: Int, element: T): Boolean { return version == 11 && element == OperatingSystem.solaris || - version == 12 && element == OperatingSystem.solaris || - version == 8 && element == Architecture.arm || - version != 8 && element == Architecture.sparcv9 || - version == 8 && element == ImageType.testimage || - version == 11 && element == ImageType.testimage || - version == 12 && element == ImageType.testimage || - element == ImageType.debugimage + version == 12 && element == OperatingSystem.solaris || + version == 8 && element == Architecture.arm || + version != 8 && element == Architecture.sparcv9 || + version == 8 && element == ImageType.testimage || + version == 11 && element == ImageType.testimage || + version == 12 && element == ImageType.testimage || + element == ImageType.debugimage } companion object { fun getPath() = "/v3/assets/feature_releases" fun getReleases(sortOrder: SortOrder): List { val body = RestAssured.given() - .`when`() - .get("${getPath()}/8/ga?sort_order=${sortOrder.name}") - .body + .`when`() + .get("${getPath()}/8/ga?sort_order=${sortOrder.name}") + .body val releasesStr = body.prettyPrint() return JsonMapper.mapper.readValue(releasesStr, JsonMapper.mapper.getTypeFactory().constructCollectionType(MutableList::class.java, Release::class.java)) @@ -122,11 +122,39 @@ class AssetsResourceFeatureReleasePathTest : AssetsPathTest() { @Test fun pagination() { - RestAssured.given() - .`when`() - .get("${getPath()}/8/ga?pageSize=1&page=1") - .then() - .statusCode(200) + .`when`() + .get("${getPath()}/8/ga?pageSize=1&page=1") + .then() + .statusCode(200) + } + + @TestFactory + fun beforeFilter(): Stream { + return listOf( + Pair("2099-01-01", 200), + Pair("2099-01-01T10:15:30", 200), + Pair("20990101", 200), + Pair("2099-12-03T10:15:30Z", 200), + Pair("2099-12-03+01:00", 200), + + Pair("2000-01-01", 404), + Pair("2000-01-01T10:15:30", 404), + Pair("20000101", 404), + Pair("2000-12-03T10:15:30Z", 404), + Pair("2000-12-03+01:00", 404), + + Pair("foo", 400) + ) + .map { + DynamicTest.dynamicTest(it.first) { + RestAssured.given() + .`when`() + .get("${getPath()}/11/ea?before=${it.first}") + .then() + .statusCode(it.second) + } + } + .stream() } }