From 9f176b9c7483c713b97a28dae45d58a27a4a4271 Mon Sep 17 00:00:00 2001 From: Thomas Edwin Santosa Date: Thu, 9 Nov 2023 12:25:07 +0700 Subject: [PATCH 1/4] Add parameters to live view controller --- .../controller/LiveViewController.kt | 132 +++++++++++++++++- .../banyuwangi/repo/BaseRepository.kt | 1 + .../banyuwangi/repo/BaseRepositoryImpl.kt | 4 + .../banyuwangi/repo/CameraRepo.kt | 11 +- 4 files changed, 141 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/katalisindonesia/banyuwangi/controller/LiveViewController.kt b/src/main/kotlin/com/katalisindonesia/banyuwangi/controller/LiveViewController.kt index 2bcf155..b43a81b 100644 --- a/src/main/kotlin/com/katalisindonesia/banyuwangi/controller/LiveViewController.kt +++ b/src/main/kotlin/com/katalisindonesia/banyuwangi/controller/LiveViewController.kt @@ -1,16 +1,29 @@ package com.katalisindonesia.banyuwangi.controller +import au.com.console.jpaspecificationdsl.and +import au.com.console.jpaspecificationdsl.equal +import au.com.console.jpaspecificationdsl.`in` +import au.com.console.jpaspecificationdsl.like +import au.com.console.jpaspecificationdsl.or +import com.katalisindonesia.banyuwangi.model.Camera +import com.katalisindonesia.banyuwangi.model.DetectionType import com.katalisindonesia.banyuwangi.repo.CameraRepo import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.data.jpa.domain.Specification import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import java.util.UUID +import javax.validation.constraints.Min @RestController @RequestMapping("/v1/live") @@ -36,18 +49,129 @@ class LiveViewController( ), ] ) - @GetMapping("/camera") + @GetMapping("/camera", "/camera/list") @PreAuthorize("hasAuthority('camera:liveview')") - fun list(): ResponseEntity>> { + fun list( + @RequestParam(required = false) keyword: String?, + + @Min(0) @RequestParam(required = false, defaultValue = "0") page: Int, + @Min(1) @RequestParam(required = false, defaultValue = "1000") size: Int, + @RequestParam(required = false, name = "location") locations: Set?, + @RequestParam(required = false, name = "id") ids: Set?, + @RequestParam(required = false, name = "type") types: Set?, + ): ResponseEntity>> { return ResponseEntity.ok( WebResponse( success = true, message = "ok", data = - cameraRepo.findWithIsActive(true, Pageable.unpaged()) - .toList() + cameraRepo.findAll( + and( + specs( + keyword = keyword, + locations = locations, + ids = ids, + types = types, + ) + ), + PageRequest.of( + page, size, Sort.by(Camera::name.name).ascending() + ) + ) .map { LiveCamera(it) } ) ) } + + @GetMapping("/camera/count") + @PreAuthorize("hasAuthority('camera:liveview')") + fun count( + @RequestParam(required = false) keyword: String?, + + @RequestParam(required = false, name = "location") locations: Set?, + @RequestParam(required = false, name = "id") ids: Set?, + @RequestParam(required = false, name = "type") types: Set?, + + ): ResponseEntity> { + return ResponseEntity.ok( + WebResponse( + success = true, + message = "ok", + data = + cameraRepo.countAll( + and( + specs( + keyword = keyword, + locations = locations, + ids = ids, + types = types, + ) + ) + ) + ) + ) + } + + @GetMapping("/location/list") + @PreAuthorize("hasAuthority('camera:liveview')") + fun locations( + @RequestParam(required = false) keyword: String?, + @Min(0) @RequestParam(required = false, defaultValue = "0") page: Int, + @Min(1) @RequestParam(required = false, defaultValue = "1000") size: Int, + ): ResponseEntity>> { + return ResponseEntity.ok( + WebResponse( + success = true, + message = "ok", + data = cameraRepo.findCameraLocations( + keyword = keyword?.wrapWithPercent() ?: "", + pageable = Pageable.ofSize(size).withPage(page) + ) + ) + ) + } + + private fun specs( + keyword: String?, + + locations: Set?, + ids: Set?, + types: Set?, + ): List> { + val list = mutableListOf>() + if (keyword != null) { + val keywordSpecs = or( + Camera::name.like(keyword.wrapWithPercent()), + Camera::location.like(keyword.wrapWithPercent()), + Camera::vmsCameraIndexCode.like(keyword.wrapWithPercent()), + ) + list.add(keywordSpecs) + } + if (!locations.isNullOrEmpty()) { + list.add(Camera::location.`in`(locations)) + } + + if (!ids.isNullOrEmpty()) { + list.add(Camera::id.`in`(ids)) + } + if (!types.isNullOrEmpty()) { + val map = mapOf( + DetectionType.FLOOD to Camera::isFlood, + DetectionType.CROWD to Camera::isCrowd, + DetectionType.STREETVENDOR to Camera::isStreetvendor, + DetectionType.TRASH to Camera::isTrash, + DetectionType.TRAFFIC to Camera::isTraffic, + ) + list.addAll( + types.mapNotNull { map[it] } + .map { it.equal(true) } + ) + } + + list.add(Camera::isActive.equal(true)) + + return list + } } + +fun String.wrapWithPercent(): String = "%$this%" diff --git a/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/BaseRepository.kt b/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/BaseRepository.kt index 62b5d30..30fc1d7 100644 --- a/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/BaseRepository.kt +++ b/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/BaseRepository.kt @@ -12,4 +12,5 @@ interface BaseRepository : JpaRepository { fun findAll(spec: Specification, offset: Long, maxResults: Int, sort: Sort): List fun findAll(spec: Specification, offset: Long, maxResults: Int): List fun findAll(spec: Specification, pageable: Pageable): List + fun countAll(spec: Specification): Long } diff --git a/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/BaseRepositoryImpl.kt b/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/BaseRepositoryImpl.kt index 92131df..6ca847c 100644 --- a/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/BaseRepositoryImpl.kt +++ b/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/BaseRepositoryImpl.kt @@ -37,4 +37,8 @@ class BaseRepositoryImpl : SimpleJpaRepository, Ba query.maxResults = maxResults return query.resultList } + + override fun countAll(spec: Specification): Long { + return super.count(spec) + } } diff --git a/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/CameraRepo.kt b/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/CameraRepo.kt index 3cdaa33..22e0830 100644 --- a/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/CameraRepo.kt +++ b/src/main/kotlin/com/katalisindonesia/banyuwangi/repo/CameraRepo.kt @@ -3,8 +3,6 @@ package com.katalisindonesia.banyuwangi.repo import com.katalisindonesia.banyuwangi.model.Camera import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.JpaSpecificationExecutor import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @@ -13,7 +11,7 @@ import java.util.Optional import java.util.UUID @Repository -interface CameraRepo : JpaRepository, JpaSpecificationExecutor { +interface CameraRepo : BaseRepository { fun getCameraByVmsCameraIndexCode(vmsCameraIndexCode: String): Optional @@ -38,4 +36,11 @@ interface CameraRepo : JpaRepository, JpaSpecificationExecutor + + @Query( + "select distinct t.location from Camera t " + + "where t.location is not null " + + "and (:keyword ='' or t.location like :keyword) order by t.location" + ) + fun findCameraLocations(keyword: String, pageable: Pageable): List } From f9f67e6e9789cabc5ee4983802d190e075db0d29 Mon Sep 17 00:00:00 2001 From: Thomas Edwin Santosa Date: Thu, 9 Nov 2023 13:20:07 +0700 Subject: [PATCH 2/4] Add log to retrofit config --- .../banyuwangi/streaming/RetrofitConfig.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/kotlin/com/katalisindonesia/banyuwangi/streaming/RetrofitConfig.kt b/src/main/kotlin/com/katalisindonesia/banyuwangi/streaming/RetrofitConfig.kt index 89334ff..0f5f3b9 100644 --- a/src/main/kotlin/com/katalisindonesia/banyuwangi/streaming/RetrofitConfig.kt +++ b/src/main/kotlin/com/katalisindonesia/banyuwangi/streaming/RetrofitConfig.kt @@ -1,12 +1,16 @@ package com.katalisindonesia.banyuwangi.streaming import com.burgstaller.okhttp.AuthenticationCacheInterceptor +import mu.KotlinLogging import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.scalars.ScalarsConverterFactory import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit +private val log = KotlinLogging.logger { } + private const val DEFAULT_TIMEOUT = 60L internal object RetrofitConfig { @@ -15,6 +19,13 @@ internal object RetrofitConfig { // user: String, // password: String ): Retrofit { + val interceptor = HttpLoggingInterceptor { message -> log.debug { message } } + if (log.isDebugEnabled) { + interceptor.level = HttpLoggingInterceptor.Level.BODY + } else { + interceptor.level = HttpLoggingInterceptor.Level.BASIC + } + // val authenticator = DigestAuthenticator(Credentials(user, password)) // val gson = GsonBuilder() @@ -22,6 +33,7 @@ internal object RetrofitConfig { // .create() val okHttpClient = OkHttpClient.Builder() + .addInterceptor(interceptor) // .authenticator(CachingAuthenticatorDecorator(authenticator, authCache)) .addInterceptor(AuthenticationCacheInterceptor(ConcurrentHashMap())) .readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) From 31323a6eabfb43e1c9af06abfc428b3e8f3adf79 Mon Sep 17 00:00:00 2001 From: Thomas Edwin Santosa Date: Thu, 9 Nov 2023 13:46:41 +0700 Subject: [PATCH 3/4] add test for live view parameters --- .../controller/LiveViewControllerTest.kt | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/src/test/kotlin/com/katalisindonesia/banyuwangi/controller/LiveViewControllerTest.kt b/src/test/kotlin/com/katalisindonesia/banyuwangi/controller/LiveViewControllerTest.kt index 1ebc1ca..635713c 100644 --- a/src/test/kotlin/com/katalisindonesia/banyuwangi/controller/LiveViewControllerTest.kt +++ b/src/test/kotlin/com/katalisindonesia/banyuwangi/controller/LiveViewControllerTest.kt @@ -21,6 +21,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.post +import java.util.UUID @ExtendWith(SpringExtension::class) @SpringBootTest @@ -174,6 +175,139 @@ class LiveViewControllerTest( } } } + @Test + fun `live view parameters`() { + val camera = Camera( + vmsCameraIndexCode = "00001", + name = "Test 01", + location = "01", + isCrowd = true, + isTraffic = false, + interior = CameraInterior( + isLiveView = null, + lastCaptureMethod = null, + isPing = null, + pingResponseTimeSec = null, + pingRawData = null, + pingLast = null, + liveViewHash = null, + liveViewUrl = null, + ) + ) + cameraRepo.saveAndFlush(camera) + + mockMvc.get("/v1/live/camera?keyword=none") { + headers { + setBearerAuth(token()) + accept = listOf(MediaType.APPLICATION_JSON) + } + }.andExpect { + status { isOk() } + content { + json( + """{ + "success": true, + "message": "ok", + "data": [] +}""", + strict = false + ) + } + } + mockMvc.get("/v1/live/camera?location=none") { + headers { + setBearerAuth(token()) + accept = listOf(MediaType.APPLICATION_JSON) + } + }.andExpect { + status { isOk() } + content { + json( + """{ + "success": true, + "message": "ok", + "data": [] +}""", + strict = false + ) + } + } + mockMvc.get("/v1/live/camera?type=TRAFFIC") { + headers { + setBearerAuth(token()) + accept = listOf(MediaType.APPLICATION_JSON) + } + }.andExpect { + status { isOk() } + content { + json( + """{ + "success": true, + "message": "ok", + "data": [] +}""", + strict = false + ) + } + } + mockMvc.get("/v1/live/camera?id=${UUID.randomUUID()}") { + headers { + setBearerAuth(token()) + accept = listOf(MediaType.APPLICATION_JSON) + } + }.andExpect { + status { isOk() } + content { + json( + """{ + "success": true, + "message": "ok", + "data": [] +}""", + strict = false + ) + } + } + mockMvc.get("/v1/live/location/list") { + headers { + setBearerAuth(token()) + accept = listOf(MediaType.APPLICATION_JSON) + } + }.andExpect { + status { isOk() } + content { + json( + """{ + "success": true, + "message": "ok", + "data": [ + "01" + ] +}""", + strict = false + ) + } + } + mockMvc.get("/v1/live/location/list?keyword=none") { + headers { + setBearerAuth(token()) + accept = listOf(MediaType.APPLICATION_JSON) + } + }.andExpect { + status { isOk() } + content { + json( + """{ + "success": true, + "message": "ok", + "data": [] +}""", + strict = false + ) + } + } + } + @Test fun `create minimum camera and get live view url`() { val camera = Camera( From 32e0033d8c3cfb06822ebad67b01fa862429e3ad Mon Sep 17 00:00:00 2001 From: Thomas Edwin Santosa Date: Thu, 9 Nov 2023 14:15:02 +0700 Subject: [PATCH 4/4] disable generateOpenApiDocs --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91a4e22..6e62cbf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Test with Gradle - run: ./gradlew --stacktrace --info check generateOpenApiDocs + run: ./gradlew --stacktrace --info check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload coverage