ExampleService.kt

package com.example.templateproject.core.service

import com.example.templateproject.api.dto.ExampleDTO
import com.example.templateproject.api.dto.PageDetails
import com.example.templateproject.client.exception.ExternalServiceException
import com.example.templateproject.client.jsonplaceholder.JsonPlaceholderService
import com.example.templateproject.core.exception.BadRequestErrorMessages
import com.example.templateproject.core.exception.BadRequestException
import com.example.templateproject.core.exception.ExecutionTimeoutException
import com.example.templateproject.core.mapper.ExampleMapper
import com.example.templateproject.core.mapper.PageConverter
import com.example.templateproject.persistence.entity.Example
import com.example.templateproject.persistence.repository.ExampleRepository
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

@Service
class ExampleService(
    @param:Value($$"${core.database.cache.enabled}") val cacheEnabled: Boolean,
    @param:Value($$"${core.wordcountcalculation.timeout.millis}") private val wordCountTimeout: Long,
    private val exampleRepository: ExampleRepository,
    private val exampleMapper: ExampleMapper,
    private val jsonPlaceholderService: JsonPlaceholderService,
    pageConverter: PageConverter,
) : AbstractService<Example, ExampleDTO>(exampleRepository, exampleMapper, pageConverter, Example::class) {
    companion object {
        private val LOGGER = LoggerFactory.getLogger(ExampleService::class.java)
        const val EXAMPLES_CACHE_NAME = "database-examples"
    }

    override fun validateEntity(entity: Example) {
        entity.name.let {
            if (!it.matches("\\d.*".toRegex())) {
                throw BadRequestException(BadRequestErrorMessages.NAME_MUST_START_WITH_A_NUMBER)
            }
        }
    }

    fun searchExamples(
        searchTerms: List<String>,
        pageable: Pageable,
    ): PageDetails<ExampleDTO> {
        val pageableToUse = getPageable(pageable)
        return exampleRepository
            .findByNameInIgnoreCase(searchTerms, pageableToUse)
            .map { exampleMapper.toDTO(it) }
            .let { pageConverter.createPageDetails(it) }
    }

    @Cacheable(
        EXAMPLES_CACHE_NAME,
        condition = "#root.target.cacheEnabled",
    )
    override fun getEntities(pageable: Pageable) = super.getEntities(pageable)

    @CacheEvict(
        value = [EXAMPLES_CACHE_NAME],
        beforeInvocation = false,
        allEntries = true,
    )
    override fun createEntity(dto: ExampleDTO) = super.createEntity(dto)

    @CacheEvict(
        value = [EXAMPLES_CACHE_NAME],
        beforeInvocation = false,
        allEntries = true,
    )
    override fun updateEntity(dto: ExampleDTO) = super.updateEntity(dto)

    @CacheEvict(
        value = [EXAMPLES_CACHE_NAME],
        beforeInvocation = false,
        allEntries = true,
    )
    override fun deleteEntity(id: Long) = super.deleteEntity(id)

    fun getWordCountForUsers(): Map<String, Int> {
        val userNameWordCountMap = ConcurrentHashMap<String, Int>()

        try {
            val users = jsonPlaceholderService.getUsers().get(wordCountTimeout, TimeUnit.MILLISECONDS)

            val futureArray =
                users
                    .map { user ->
                        jsonPlaceholderService.getPostsByUserId(user.id).thenAccept { posts ->
                            val totalWords = posts.sumOf { it.body.split("\\s+".toRegex()).size }
                            userNameWordCountMap[user.username] =
                                userNameWordCountMap.getOrDefault(user.username, 0) + totalWords
                        }
                    }.toTypedArray()

            CompletableFuture
                .allOf(*futureArray)
                .thenRun {
                    LOGGER.info("Calculated word count for users: {}", userNameWordCountMap)
                }.get(wordCountTimeout, TimeUnit.MILLISECONDS)
        } catch (e: TimeoutException) {
            throw ExecutionTimeoutException("Calculating word count for users", e.message)
        } catch (ex: ExecutionException) {
            val cause = ex.cause!!
            throw cause as? ExternalServiceException
                ?: ExternalServiceException(cause, cause.message!!, jsonPlaceholderService.clientId)
        }

        return userNameWordCountMap.entries
            .sortedByDescending { it.value }
            .associate { it.key to it.value }
    }
}