diff --git a/build.gradle.kts b/build.gradle.kts index 57f913a..87231ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ plugins { id("application") id("org.springframework.boot") version "3.3.2" id("io.spring.dependency-management") version "1.1.6" + id("org.springdoc.openapi-gradle-plugin") version "1.9.0" } group = "com.github.nenadjakic.ocr.studio" @@ -29,6 +30,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-mongodb") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.modelmapper:modelmapper:3.2.1") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") implementation("net.sourceforge.tess4j:tess4j:5.12.0") diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/config/SecurityConfig.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/config/SecurityConfig.kt new file mode 100644 index 0000000..96ec8d1 --- /dev/null +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/config/SecurityConfig.kt @@ -0,0 +1,34 @@ +package com.github.nenadjakic.ocr.studio.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.UrlBasedCorsConfigurationSource + + +@Configuration +@EnableWebSecurity +class SecurityConfig { + + @Bean + fun filterChain(httpSecurity: HttpSecurity): SecurityFilterChain { + httpSecurity.cors { it.configurationSource(corsConfigurationSource()) } + httpSecurity.authorizeHttpRequests { it.anyRequest().permitAll() } + return httpSecurity.build() + } + + fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = listOf("*") + configuration.allowedMethods = listOf("*") + configuration.allowedHeaders = listOf("*") + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source + } +} diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/controller/AnalyticsController.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/controller/AnalyticsController.kt new file mode 100644 index 0000000..63db8e5 --- /dev/null +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/controller/AnalyticsController.kt @@ -0,0 +1,42 @@ +package com.github.nenadjakic.ocr.studio.controller + +import com.github.nenadjakic.ocr.studio.dto.StatusCount +import com.github.nenadjakic.ocr.studio.service.AnalyticsService +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.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Analytics controller", description = "API endpoints for analytics.") +@RestController +@RequestMapping("/analytics") +class AnalyticsController(private val analyticsService: AnalyticsService) { + + @Operation( + operationId = "getCountByStatus", + summary = "Get count by status.", + description = "Returns count by status information.") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Successfully retrieved count by status.") + ] + ) + @GetMapping(value = ["/count-by-status"], produces = [org.springframework.http.MediaType.APPLICATION_JSON_VALUE]) + fun getCountByStatus(): ResponseEntity> = ResponseEntity.ok(analyticsService.getCountByStatus()) + + @Operation( + operationId = "getAverageInDocuments", + summary = "Get average count across inDocuments.", + description = "Returns average count across inDocuments.") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Successfully retrieved average count across inDocuments.") + ] + ) + @GetMapping(value = ["/average-in-documents"], produces = [org.springframework.http.MediaType.APPLICATION_JSON_VALUE]) + fun getAverageInDocuments(): ResponseEntity = ResponseEntity.ok(analyticsService.getAverageInDocuments()) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/dto/SchedulerConfigRequest.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/dto/SchedulerConfigRequest.kt index 6e15cd1..17d35ea 100644 --- a/src/main/kotlin/com/github/nenadjakic/ocr/studio/dto/SchedulerConfigRequest.kt +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/dto/SchedulerConfigRequest.kt @@ -1,5 +1,5 @@ package com.github.nenadjakic.ocr.studio.dto -import java.time.ZonedDateTime +import java.time.LocalDateTime -data class SchedulerConfigRequest(var startDateTime: ZonedDateTime? = null) \ No newline at end of file +data class SchedulerConfigRequest(var startDateTime: LocalDateTime? = null) \ No newline at end of file diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/dto/StatusCount.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/dto/StatusCount.kt new file mode 100644 index 0000000..00da0fa --- /dev/null +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/dto/StatusCount.kt @@ -0,0 +1,8 @@ +package com.github.nenadjakic.ocr.studio.dto + +import com.github.nenadjakic.ocr.studio.entity.Status + +data class StatusCount( + val status: Status, + val count: Long +) diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/entity/SchedulerConfig.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/entity/SchedulerConfig.kt index 289e70b..31ab2de 100644 --- a/src/main/kotlin/com/github/nenadjakic/ocr/studio/entity/SchedulerConfig.kt +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/entity/SchedulerConfig.kt @@ -1,7 +1,7 @@ package com.github.nenadjakic.ocr.studio.entity -import java.time.ZonedDateTime +import java.time.LocalDateTime class SchedulerConfig( - var startDateTime: ZonedDateTime? = null + var startDateTime: LocalDateTime? = null ) \ No newline at end of file diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/Executor.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/Executor.kt index d2c48f3..3c6453e 100644 --- a/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/Executor.kt +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/Executor.kt @@ -1,10 +1,10 @@ package com.github.nenadjakic.ocr.studio.executor -import java.time.ZonedDateTime +import java.time.LocalDateTime import java.util.UUID interface Executor : Runnable { val id: UUID - val startDateTime: ZonedDateTime? + val startDateTime: LocalDateTime? val progressInfo: ProgressInfo } \ No newline at end of file diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/OcrExecutor.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/OcrExecutor.kt index 69f8de1..4ea17f2 100644 --- a/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/OcrExecutor.kt +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/OcrExecutor.kt @@ -17,14 +17,14 @@ import org.apache.pdfbox.rendering.PDFRenderer import org.slf4j.LoggerFactory import java.io.* import java.nio.file.Path -import java.time.ZonedDateTime +import java.time.LocalDateTime import java.util.* import javax.imageio.ImageIO import javax.xml.parsers.SAXParserFactory class OcrExecutor( override val id: UUID, - override val startDateTime: ZonedDateTime?, + override val startDateTime: LocalDateTime?, private val ocrProperties: OcrProperties, private val tesseract: ITesseract, private val taskRepository: TaskRepository, diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/ParallelizationManagerImpl.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/ParallelizationManagerImpl.kt index 0eb33bc..e83d9ce 100644 --- a/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/ParallelizationManagerImpl.kt +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/executor/ParallelizationManagerImpl.kt @@ -4,7 +4,8 @@ import com.github.nenadjakic.ocr.studio.exception.ConfigurationException import org.springframework.scheduling.TaskScheduler import org.springframework.stereotype.Service import java.time.Instant -import java.time.ZonedDateTime +import java.time.LocalDateTime +import java.time.ZoneOffset import java.util.* import java.util.concurrent.ScheduledFuture @@ -16,12 +17,12 @@ class ParallelizationManagerImpl( private val futures: MutableMap> = mutableMapOf() override fun schedule(executor: Executor) { - if (executor.startDateTime?.isBefore(ZonedDateTime.now()) == true) { + if (executor.startDateTime?.isBefore(LocalDateTime.now()) == true) { throw ConfigurationException("Start time is wrong. Cannot schedule task.") } val future: ScheduledFuture = if (executor.startDateTime != null) { - taskScheduler.schedule({ executor.run() }, executor.startDateTime!!.toInstant()) + taskScheduler.schedule({ executor.run() }, executor.startDateTime!!.toInstant(ZoneOffset.UTC)) } else { taskScheduler.schedule({ executor.run() }, Instant.now().plusSeconds(30L)) } diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/repository/TaskRepository.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/repository/TaskRepository.kt index 44e90a9..7af6cb8 100644 --- a/src/main/kotlin/com/github/nenadjakic/ocr/studio/repository/TaskRepository.kt +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/repository/TaskRepository.kt @@ -1,8 +1,10 @@ package com.github.nenadjakic.ocr.studio.repository +import com.github.nenadjakic.ocr.studio.dto.StatusCount import com.github.nenadjakic.ocr.studio.entity.OcrConfig import com.github.nenadjakic.ocr.studio.entity.SchedulerConfig import com.github.nenadjakic.ocr.studio.entity.Task +import org.springframework.data.mongodb.repository.Aggregation import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.data.mongodb.repository.Query import org.springframework.data.mongodb.repository.Update @@ -21,4 +23,17 @@ interface TaskRepository : MongoRepository { @Query(value = "{ 'id': ?0 }") @Update("{ 'ocrConfig.language': ?1 }") fun updateLanguageById(id: UUID, language: String): Int + + @Aggregation(pipeline = [ + "{ '\$group': { '_id': '\$ocrProgress.status', 'count': { '\$sum': 1 } } }", + "{ '\$project': { 'status': '\$_id', 'count': 1, '_id': 0 } }" + ]) + fun countTasksByStatus(): List + + @Aggregation(pipeline = [ + "{ '\$project': { 'numInDocuments': { '\$size': '\$inDocuments' } } }", + "{ '\$group': { '_id': null, 'averageCount': { '\$avg': '\$numInDocuments' } } }", + "{ '\$project': { '_id': 0, 'averageCount': 1 } }" + ]) + fun averageInDocuments(): Long } \ No newline at end of file diff --git a/src/main/kotlin/com/github/nenadjakic/ocr/studio/service/AnalyticsService.kt b/src/main/kotlin/com/github/nenadjakic/ocr/studio/service/AnalyticsService.kt new file mode 100644 index 0000000..85c182f --- /dev/null +++ b/src/main/kotlin/com/github/nenadjakic/ocr/studio/service/AnalyticsService.kt @@ -0,0 +1,14 @@ +package com.github.nenadjakic.ocr.studio.service + +import com.github.nenadjakic.ocr.studio.dto.StatusCount +import com.github.nenadjakic.ocr.studio.repository.TaskRepository +import org.springframework.stereotype.Service + +@Service +class AnalyticsService( + private val taskRepository: TaskRepository +) { + fun getCountByStatus(): List = taskRepository.countTasksByStatus() + + fun getAverageInDocuments(): Long = taskRepository.averageInDocuments() +} \ No newline at end of file