From 045102b767802d678bd6d9b85b4dd6a152f460d9 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Wed, 13 Nov 2024 16:44:12 +0300 Subject: [PATCH 1/2] chore: claude code improvements --- .../FirebaseTokenAuthenticationFilter.kt | 44 ++++++----- .../com/epam/brn/repo/SeriesRepository.kt | 9 ++- .../epam/brn/repo/StudyHistoryRepository.kt | 47 ++++++++---- .../com/epam/brn/repo/SubGroupRepository.kt | 6 +- .../com/epam/brn/repo/TaskRepository.kt | 27 ++++--- .../epam/brn/repo/UserAccountRepository.kt | 49 +++++++----- .../epam/brn/service/ExerciseGroupsService.kt | 17 +++-- .../epam/brn/service/StudyHistoryService.kt | 33 +++----- .../com/epam/brn/service/SubGroupService.kt | 14 ++-- .../com/epam/brn/service/TaskService.kt | 35 ++++----- .../epam/brn/service/cloud/AwsCloudService.kt | 39 +++++----- .../brn/service/cloud/GoogleCloudService.kt | 13 ++-- .../epam/brn/service/impl/RoleServiceImpl.kt | 8 +- .../service/impl/UserAccountServiceImpl.kt | 12 ++- .../service/impl/UserAnalyticsServiceImpl.kt | 76 +++++++++++-------- .../impl/UserDayStatisticsService.kt | 19 ++--- .../impl/UserMonthStatisticsService.kt | 25 +++--- 17 files changed, 266 insertions(+), 207 deletions(-) diff --git a/src/main/kotlin/com/epam/brn/auth/filter/FirebaseTokenAuthenticationFilter.kt b/src/main/kotlin/com/epam/brn/auth/filter/FirebaseTokenAuthenticationFilter.kt index bca34d009..5995db845 100644 --- a/src/main/kotlin/com/epam/brn/auth/filter/FirebaseTokenAuthenticationFilter.kt +++ b/src/main/kotlin/com/epam/brn/auth/filter/FirebaseTokenAuthenticationFilter.kt @@ -42,31 +42,21 @@ class FirebaseTokenAuthenticationFilter( private fun verifyToken(request: HttpServletRequest) { val token: String? = tokenHelperUtils.getBearerToken(request) + if (token.isNullOrEmpty()) { + return + } + try { val decodedToken: FirebaseToken = firebaseAuth.verifyIdToken(token, true) try { val user: UserDetails = brainUpUserDetailsService.loadUserByUsername(decodedToken.email) - val authentication = UsernamePasswordAuthenticationToken( - user, - UserAccountCredentials(decodedToken, token), - user.authorities - ) - authentication.details = WebAuthenticationDetailsSource().buildDetails(request) - SecurityContextHolder.getContext().authentication = authentication + setAuthentication(user, decodedToken, token, request) } catch (e: UsernameNotFoundException) { log.warn("User with email: ${decodedToken.email} doesn't exist: create it") - val firebaseUserRecord = firebaseUserService.getUserByUuid(decodedToken.uid) - if (firebaseUserRecord != null) { - val createUser = userAccountService.createUser(firebaseUserRecord) - val user: UserDetails = brainUpUserDetailsService.loadUserByUsername(createUser.email) - val authentication = UsernamePasswordAuthenticationToken( - user, - UserAccountCredentials(decodedToken, token), - user.authorities - ) - authentication.details = WebAuthenticationDetailsSource().buildDetails(request) - SecurityContextHolder.getContext().authentication = authentication - } + val firebaseUserRecord = firebaseUserService.getUserByUuid(decodedToken.uid) ?: return + val createdUser = userAccountService.createUser(firebaseUserRecord) + val user: UserDetails = brainUpUserDetailsService.loadUserByUsername(createdUser.email) + setAuthentication(user, decodedToken, token, request) } } catch (e: FirebaseAuthException) { log.error("Error while validate token: ${e.message}", e) @@ -74,4 +64,20 @@ class FirebaseTokenAuthenticationFilter( log.error("Error: ${e.message}", e) } } + + private fun setAuthentication( + user: UserDetails, + decodedToken: FirebaseToken, + token: String, + request: HttpServletRequest + ) { + val authentication = UsernamePasswordAuthenticationToken( + user, + UserAccountCredentials(decodedToken, token), + user.authorities + ).apply { + details = WebAuthenticationDetailsSource().buildDetails(request) + } + SecurityContextHolder.getContext().authentication = authentication + } } diff --git a/src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt b/src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt index de448e623..4859d4d0a 100644 --- a/src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt +++ b/src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt @@ -4,22 +4,23 @@ import com.epam.brn.model.Series import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.CrudRepository import org.springframework.stereotype.Repository +import java.util.Optional @Repository interface SeriesRepository : CrudRepository { fun findByNameLike(name: String): List - @Query("select distinct s from Series s where s.type=?1 and s.exerciseGroup.locale=?2") + @Query("select distinct s from Series s left join fetch s.exerciseGroup where s.type = :type and s.exerciseGroup.locale = :locale") fun findByTypeAndLocale(type: String, locale: String): Series? fun findByTypeAndName(type: String, name: String): Series? fun findByNameIn(names: List): List - @Query("select distinct s from Series s where s.exerciseGroup.id=?1") + @Query("select distinct s from Series s left join fetch s.exerciseGroup where s.exerciseGroup.id = :groupId") fun findByExerciseGroupLike(groupId: Long): List -// @Query("select distinct s from Series s left JOIN FETCH s.exercises where s.id=?1") -// fun findSeriesWithExercisesById(groupId: Long): Optional + @Query("select distinct s from Series s left join fetch s.exercises where s.id = :seriesId") + fun findSeriesWithExercisesById(seriesId: Long): Optional } diff --git a/src/main/kotlin/com/epam/brn/repo/StudyHistoryRepository.kt b/src/main/kotlin/com/epam/brn/repo/StudyHistoryRepository.kt index 223a45948..e4aee4eab 100644 --- a/src/main/kotlin/com/epam/brn/repo/StudyHistoryRepository.kt +++ b/src/main/kotlin/com/epam/brn/repo/StudyHistoryRepository.kt @@ -25,23 +25,31 @@ interface StudyHistoryRepository : CrudRepository { fun getDoneExercisesIdList(@Param("userId") userId: Long): List @Query( - "SELECT s FROM StudyHistory s " + - " WHERE (s.userAccount.id, s.startTime) " + - " IN (SELECT userAccount.id, max(startTime) " + - " FROM StudyHistory " + - " GROUP BY exercise.id, userAccount.id " + - " HAVING userAccount.id = :userId)" + """ + SELECT s FROM StudyHistory s + WHERE s.userAccount.id = :userId + AND s.startTime = ( + SELECT MAX(sh.startTime) + FROM StudyHistory sh + WHERE sh.userAccount.id = s.userAccount.id + AND sh.exercise.id = s.exercise.id + ) + """ ) fun findLastByUserAccountId(userId: Long): List @Query( - "SELECT s FROM StudyHistory s " + - " WHERE (s.userAccount.id, s.startTime) " + - " IN (SELECT userAccount.id, max(startTime) " + - " FROM StudyHistory " + - " WHERE exercise.subGroup.id = :subGroupId " + - " GROUP BY exercise.id, userAccount.id " + - " HAVING userAccount.id = :userId)" + """ + SELECT s FROM StudyHistory s + WHERE s.exercise.subGroup.id = :subGroupId + AND s.userAccount.id = :userId + AND s.startTime = ( + SELECT MAX(sh.startTime) + FROM StudyHistory sh + WHERE sh.userAccount.id = s.userAccount.id + AND sh.exercise.id = s.exercise.id + ) + """ ) fun findLastBySubGroupAndUserAccount(subGroupId: Long, userId: Long): List @@ -93,9 +101,16 @@ interface StudyHistoryRepository : CrudRepository { ): List @Query( - "SELECT MIN(s.startTime) AS firstStudy, MAX(s.startTime) AS lastStudy," + - " COALESCE(SUM(s.spentTimeInSeconds), 0) AS spentTime, COUNT (DISTINCT s.exercise.id) as doneExercises" + - " FROM StudyHistory s WHERE user_id = :userId" + """ + SELECT NEW com.epam.brn.model.projection.UserStatisticView( + MIN(s.startTime), + MAX(s.startTime), + COALESCE(SUM(s.spentTimeInSeconds), 0), + COUNT(DISTINCT s.exercise.id) + ) + FROM StudyHistory s + WHERE s.userAccount.id = :userId + """ ) fun getStatisticsByUserAccountId(userId: Long?): UserStatisticView diff --git a/src/main/kotlin/com/epam/brn/repo/SubGroupRepository.kt b/src/main/kotlin/com/epam/brn/repo/SubGroupRepository.kt index d1f6adb19..400013352 100644 --- a/src/main/kotlin/com/epam/brn/repo/SubGroupRepository.kt +++ b/src/main/kotlin/com/epam/brn/repo/SubGroupRepository.kt @@ -11,16 +11,16 @@ interface SubGroupRepository : CrudRepository { fun findByNameLike(name: String): List - @Query("select distinct s from SubGroup s where s.code=?1 and s.series.exerciseGroup.locale=?2") + @Query("select distinct s from SubGroup s join fetch s.series ser join fetch ser.exerciseGroup where s.code = ?1 and ser.exerciseGroup.locale = ?2") fun findByCodeAndLocale(code: String, locale: String): SubGroup? fun findByNameAndLevelAndSeries(name: String, level: Int, series: Series): SubGroup? fun findByNameAndLevel(name: String, level: Int): SubGroup? - @Query("select distinct s from SubGroup s where s.name=?1 and s.level=?2 and s.series.exerciseGroup.locale=?3") + @Query("select distinct s from SubGroup s join fetch s.series ser join fetch ser.exerciseGroup where s.name = ?1 and s.level = ?2 and ser.exerciseGroup.locale = ?3") fun findByNameAndLevelAndLocale(name: String, level: Int, locale: String): SubGroup? - @Query("select distinct s from SubGroup s where s.series.id=?1") + @Query("select distinct s from SubGroup s join fetch s.series where s.series.id = ?1") fun findBySeriesId(seriesId: Long): List } diff --git a/src/main/kotlin/com/epam/brn/repo/TaskRepository.kt b/src/main/kotlin/com/epam/brn/repo/TaskRepository.kt index 93a1e2b46..586b99a86 100644 --- a/src/main/kotlin/com/epam/brn/repo/TaskRepository.kt +++ b/src/main/kotlin/com/epam/brn/repo/TaskRepository.kt @@ -9,18 +9,27 @@ import java.util.Optional @Repository interface TaskRepository : JpaRepository { - @Query("select DISTINCT t FROM Task t left JOIN FETCH t.answerOptions") + @Query(""" + select DISTINCT t FROM Task t + left JOIN FETCH t.answerOptions ao + WHERE t IN (select t2 from Task t2) + """) fun findAllTasksWithJoinedAnswers(): List - @Query("select DISTINCT t FROM Task t left JOIN FETCH t.answerOptions where t.exercise.id = ?1") + @Query(""" + select DISTINCT t FROM Task t + left JOIN FETCH t.answerOptions ao + where t.exercise.id = :id + AND t IN (select t2 from Task t2 where t2.exercise.id = :id) + """) fun findTasksByExerciseIdWithJoinedAnswers(id: Long): List - @Query( - "select DISTINCT t " + - "FROM Task t " + - "left JOIN FETCH t.answerParts " + - "left JOIN FETCH t.answerOptions " + - "where t.id = ?1" - ) + @Query(""" + select DISTINCT t FROM Task t + left JOIN FETCH t.answerParts ap + left JOIN FETCH t.answerOptions ao + where t.id = :id + AND t IN (select t2 from Task t2 where t2.id = :id) + """) override fun findById(id: Long): Optional } diff --git a/src/main/kotlin/com/epam/brn/repo/UserAccountRepository.kt b/src/main/kotlin/com/epam/brn/repo/UserAccountRepository.kt index 107e29fca..5999a6f9f 100644 --- a/src/main/kotlin/com/epam/brn/repo/UserAccountRepository.kt +++ b/src/main/kotlin/com/epam/brn/repo/UserAccountRepository.kt @@ -10,49 +10,62 @@ import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime import java.util.Optional +import org.springframework.data.repository.query.Param @Repository interface UserAccountRepository : JpaRepository { @Query( - """select DISTINCT u FROM UserAccount u left JOIN FETCH u.roleSet - left JOIN FETCH u.headphones where u.fullName = ?1""" + """select u FROM UserAccount u + JOIN FETCH u.roleSet + LEFT JOIN FETCH u.headphones + where u.fullName = :fullName""" ) - fun findUserAccountByName(fullName: String): Optional + fun findUserAccountByName(@Param("fullName") fullName: String): Optional @Query( - """select DISTINCT u FROM UserAccount u left JOIN FETCH u.roleSet - left JOIN FETCH u.headphones where LOWER(u.email) = LOWER( ?1)""" + """select u FROM UserAccount u + JOIN FETCH u.roleSet + LEFT JOIN FETCH u.headphones + where LOWER(u.email) = LOWER(:email)""" ) - fun findUserAccountByEmail(email: String): Optional + fun findUserAccountByEmail(@Param("email") email: String): Optional @Query( - """select DISTINCT u FROM UserAccount u left JOIN FETCH u.roleSet - left JOIN FETCH u.headphones where u.id = ?1""" + """select u FROM UserAccount u + JOIN FETCH u.roleSet + LEFT JOIN FETCH u.headphones + where u.id = :id""" ) - fun findUserAccountById(id: Long): Optional + fun findUserAccountById(@Param("id") id: Long): Optional @Query( - """select DISTINCT u FROM UserAccount u left JOIN FETCH u.roleSet - left JOIN FETCH u.headphones where u.doctor = ?1""" + """select u FROM UserAccount u + JOIN FETCH u.roleSet + LEFT JOIN FETCH u.headphones + where u.doctor = :doctor""" ) - fun findUserAccountsByDoctor(doctor: UserAccount): List + fun findUserAccountsByDoctor(@Param("doctor") doctor: UserAccount): List @Query( - """select DISTINCT u FROM UserAccount u left JOIN FETCH u.roleSet - left JOIN FETCH u.headphones where u.doctor.id = ?1""" + """select u FROM UserAccount u + JOIN FETCH u.roleSet + LEFT JOIN FETCH u.headphones + where u.doctor.id = :doctorId""" ) - fun findUserAccountsByDoctorId(doctorId: Long): List + fun findUserAccountsByDoctorId(@Param("doctorId") doctorId: Long): List fun findByUserId(uuid: String): UserAccount? fun findAllByUserIdIsNullAndIsFirebaseErrorIsFalse(pageable: Pageable): Page @Query( - """select DISTINCT u FROM UserAccount u left JOIN FETCH u.roleSet roles - left JOIN FETCH u.headphones where roles.name = :roleName""" + """select u FROM UserAccount u + JOIN FETCH u.roleSet roles + LEFT JOIN FETCH u.headphones + where roles.name = :roleName""" ) - fun findUsersAccountsByRole(roleName: String): List + fun findUsersAccountsByRole(@Param("roleName") roleName: String): List @Transactional @Modifying diff --git a/src/main/kotlin/com/epam/brn/service/ExerciseGroupsService.kt b/src/main/kotlin/com/epam/brn/service/ExerciseGroupsService.kt index fc448c5da..007c684aa 100644 --- a/src/main/kotlin/com/epam/brn/service/ExerciseGroupsService.kt +++ b/src/main/kotlin/com/epam/brn/service/ExerciseGroupsService.kt @@ -17,8 +17,10 @@ class ExerciseGroupsService( fun findAllGroups(): List { log.debug("Searching all groups") - val groups: List = exerciseGroupRepository.findAll() - return groups.map { group -> group.toDto() } + return exerciseGroupRepository.findAll() + .asSequence() + .map { it.toDto() } + .toList() } fun findGroupDtoById(groupId: Long): ExerciseGroupDto { @@ -30,10 +32,13 @@ class ExerciseGroupsService( fun findByLocale(locale: String): List { log.debug("Searching groups by locale=$locale") - if (locale.isEmpty()) - return exerciseGroupRepository.findAll().map { group -> group.toDto() } - return exerciseGroupRepository.findByLocale(locale) - .map { group -> group.toDto() } + return when { + locale.isBlank() -> findAllGroups() + else -> exerciseGroupRepository.findByLocale(locale) + .asSequence() + .map { it.toDto() } + .toList() + } } fun save(exerciseGroup: ExerciseGroup): ExerciseGroup { diff --git a/src/main/kotlin/com/epam/brn/service/StudyHistoryService.kt b/src/main/kotlin/com/epam/brn/service/StudyHistoryService.kt index 4ab18293a..a922df287 100644 --- a/src/main/kotlin/com/epam/brn/service/StudyHistoryService.kt +++ b/src/main/kotlin/com/epam/brn/service/StudyHistoryService.kt @@ -79,30 +79,21 @@ class StudyHistoryService( private fun calculateUserDailyDetailStatistics(studyHistories: List): MutableList { - val result = mutableListOf() - studyHistories + return studyHistories .groupBy { it.exercise.subGroup!!.series.name } - .forEach { (seriesName, histories) -> - val allDoneExercisesCount = histories.size - val studyHistoryByExercise = histories - .groupBy { it.exercise.id } - val uniqueDoneExercisesCount = studyHistoryByExercise - .count() - val doneExercisesSuccessfullyFromFirstTime = studyHistoryByExercise - .count { it.value.size == 1 } - val listenWordsCount = histories.sumOf { it.tasksCount.toInt() } - val seconds = histories.sumOf { it.spentTimeInSeconds ?: 0L } - val userDailyDetailStatisticsDto = UserDailyDetailStatisticsDto( + .map { (seriesName, histories) -> + val exerciseGroups = histories.groupBy { it.exercise.id } + UserDailyDetailStatisticsDto( seriesName = seriesName, - allDoneExercises = allDoneExercisesCount, - uniqueDoneExercises = uniqueDoneExercisesCount, - doneExercisesSuccessfullyFromFirstTime = doneExercisesSuccessfullyFromFirstTime, - repeatedExercises = allDoneExercisesCount - doneExercisesSuccessfullyFromFirstTime, - listenWordsCount = listenWordsCount, - duration = (seconds.toDouble() / 60).toDuration(DurationUnit.MINUTES) + allDoneExercises = histories.size, + uniqueDoneExercises = exerciseGroups.size, + doneExercisesSuccessfullyFromFirstTime = exerciseGroups.count { it.value.size == 1 }, + repeatedExercises = histories.size - exerciseGroups.count { it.value.size == 1 }, + listenWordsCount = histories.sumOf { it.tasksCount.toInt() }, + duration = (histories.sumOf { it.spentTimeInSeconds ?: 0L }.toDouble() / 60) + .toDuration(DurationUnit.MINUTES) ) - result.add(userDailyDetailStatisticsDto) } - return result + .toMutableList() } } diff --git a/src/main/kotlin/com/epam/brn/service/SubGroupService.kt b/src/main/kotlin/com/epam/brn/service/SubGroupService.kt index ece38c15a..72297afef 100644 --- a/src/main/kotlin/com/epam/brn/service/SubGroupService.kt +++ b/src/main/kotlin/com/epam/brn/service/SubGroupService.kt @@ -25,9 +25,7 @@ class SubGroupService( log.debug("Try to find subGroups for seriesId=$seriesId") return subGroupRepository .findBySeriesId(seriesId) - .parallelStream() .map { subGroup -> toSubGroupResponse(subGroup) } - .collect(Collectors.toList()) .sortedWith(compareBy({ it.level }, { it.withPictures })) } @@ -59,15 +57,17 @@ class SubGroupService( fun addSubGroupToSeries(subGroupRequest: SubGroupRequest, seriesId: Long): SubGroupResponse { log.debug("try to find subgroup by name=${subGroupRequest.name} and the level=${subGroupRequest.level}") - val existSubGroup = subGroupRepository.findByNameAndLevel(subGroupRequest.name, subGroupRequest.level!!) + val level = subGroupRequest.level ?: throw IllegalArgumentException("Level is required") + val existSubGroup = subGroupRepository.findByNameAndLevel(subGroupRequest.name, level) if (existSubGroup != null) - throw IllegalArgumentException("The subgroup with name=${subGroupRequest.name} and the level=${subGroupRequest.level} already exists!") + throw IllegalArgumentException("The subgroup with name=${subGroupRequest.name} and the level=$level already exists!") + log.debug("try to find Series by Id=$seriesId") val series = seriesRepository.findById(seriesId) .orElseThrow { EntityNotFoundException("No series was found by id=$seriesId.") } - val subGroup = subGroupRequest.toModel(series) - val savedSubGroup = subGroupRepository.save(subGroup) - return toSubGroupResponse(savedSubGroup) + + return subGroupRepository.save(subGroupRequest.toModel(series)) + .let { toSubGroupResponse(it) } } fun toSubGroupResponse(subGroup: SubGroup): SubGroupResponse { diff --git a/src/main/kotlin/com/epam/brn/service/TaskService.kt b/src/main/kotlin/com/epam/brn/service/TaskService.kt index afdb4a064..bf4e562c9 100644 --- a/src/main/kotlin/com/epam/brn/service/TaskService.kt +++ b/src/main/kotlin/com/epam/brn/service/TaskService.kt @@ -85,13 +85,9 @@ class TaskService( } } -val vowels = "а,е,ё,и,о,у,э,ы,ю,я".toCharArray() +private val vowelSet = setOf('а', 'е', 'ё', 'и', 'о', 'у', 'э', 'ы', 'ю', 'я') -fun String.findSyllableCount(): Int { - var syllableCount = 0 - this.toCharArray().forEach { if (vowels.contains(it)) syllableCount++ } - return syllableCount -} +fun String.findSyllableCount(): Int = count { it in vowelSet } fun Task.toDetailWordsTaskDto(exerciseType: ExerciseType) = TaskResponse( id = id!!, @@ -103,18 +99,19 @@ fun Task.toDetailWordsTaskDto(exerciseType: ExerciseType) = TaskResponse( ) fun MutableSet.toResourceDtoSet(): HashSet { - val mapVowelCountToWord: Map> = - this.groupBy { resource -> resource.word.findSyllableCount() } - val resultDtoSet = mutableSetOf() - mapVowelCountToWord.keys - .sorted() - .forEachIndexed { index, vowelCount -> - val resources = mapVowelCountToWord[vowelCount]?.map { it.toResponse() } - resources?.forEach { - it.columnNumber = index - it.soundsCount = vowelCount - } - resultDtoSet.addAll(resources ?: emptySet()) + val resultDtoSet = this.mapTo(ArrayList(size)) { resource -> + val syllableCount = resource.word.findSyllableCount() + resource.toResponse().apply { + soundsCount = syllableCount } - return resultDtoSet.toHashSet() + } + + val syllableCounts = resultDtoSet.map { it.soundsCount }.distinct().sorted() + val syllableToColumn = syllableCounts.withIndex().associate { (index, count) -> count to index } + + resultDtoSet.forEach { response -> + response.columnNumber = syllableToColumn[response.soundsCount]!! + } + + return HashSet(resultDtoSet) } diff --git a/src/main/kotlin/com/epam/brn/service/cloud/AwsCloudService.kt b/src/main/kotlin/com/epam/brn/service/cloud/AwsCloudService.kt index c5c2986a8..8a4730895 100644 --- a/src/main/kotlin/com/epam/brn/service/cloud/AwsCloudService.kt +++ b/src/main/kotlin/com/epam/brn/service/cloud/AwsCloudService.kt @@ -34,19 +34,18 @@ class AwsCloudService(@Autowired private val awsConfig: AwsConfig, @Autowired pr companion object { private const val FOLDER_DELIMITER = "/" + private val mapperIndented: ObjectWriter = run { + val indenter = DefaultIndenter().withLinefeed("\r\n") + val printer = DefaultPrettyPrinter().withObjectIndenter(indenter) + val objectMapper = ObjectMapper() + objectMapper.enable(SerializationFeature.INDENT_OUTPUT) + objectMapper.writer(printer) + } } private val log = logger() - private final val mapperIndented: ObjectWriter - - init { - val indenter = DefaultIndenter().withLinefeed("\r\n") - val printer = DefaultPrettyPrinter().withObjectIndenter(indenter) - val objectMapper = ObjectMapper() - objectMapper.enable(SerializationFeature.INDENT_OUTPUT) - mapperIndented = objectMapper.writer(printer) - } + private val hmacSHA256: Mac = Mac.getInstance("HmacSHA256") override fun bucketUrl(): String = awsConfig.bucketLink @@ -172,21 +171,20 @@ class AwsCloudService(@Autowired private val awsConfig: AwsConfig, @Autowired pr return key } - private fun getFolders(prefix: String): ArrayList { + private fun getFolders(prefix: String): List { val listObjectsV2Request = ListObjectsV2Request.builder() .delimiter(FOLDER_DELIMITER) .prefix(prefix) .bucket(awsConfig.bucketName) .build() val result = s3Client.listObjectsV2(listObjectsV2Request) - val matchingKeys = result.commonPrefixes() - val folders: ArrayList = ArrayList() - matchingKeys.forEach { - val currentPrefix = it.prefix() - folders.add(currentPrefix) - folders.addAll(getFolders(prefix + currentPrefix)) + return buildList { + result.commonPrefixes().forEach { + val currentPrefix = it.prefix() + add(currentPrefix) + addAll(getFolders(prefix + currentPrefix)) + } } - return folders } private fun signature(conditions: AwsConfig.Conditions): Map { @@ -262,9 +260,10 @@ class AwsCloudService(@Autowired private val awsConfig: AwsConfig, @Autowired pr } private fun hmacSHA256(data: String, key: ByteArray): ByteArray { - val mac = Mac.getInstance("HmacSHA256") - mac.init(SecretKeySpec(key, "HmacSHA256")) - return mac.doFinal(data.toByteArray()) + synchronized(hmacSHA256) { + hmacSHA256.init(SecretKeySpec(key, "HmacSHA256")) + return hmacSHA256.doFinal(data.toByteArray()) + } } private fun toHex(bytes: ByteArray): String = bytes.joinToString("") { "%02x".format(it) } diff --git a/src/main/kotlin/com/epam/brn/service/cloud/GoogleCloudService.kt b/src/main/kotlin/com/epam/brn/service/cloud/GoogleCloudService.kt index f99435168..f42c230c5 100644 --- a/src/main/kotlin/com/epam/brn/service/cloud/GoogleCloudService.kt +++ b/src/main/kotlin/com/epam/brn/service/cloud/GoogleCloudService.kt @@ -19,13 +19,16 @@ class GoogleCloudService(@Autowired private val cloudConfig: GoogleCloudConfig) override fun getStorageFolders(): List { val blobs = cloudConfig.storage!!.get(cloudConfig.bucketName).list() - val folders: MutableSet = TreeSet() + for (blob in blobs.iterateAll()) { - var fileName = blob.name.replaceAfterLast("/", "") - while (fileName.contains("/")) { - folders.add(fileName) - fileName = fileName.removeSuffix("/").replaceAfterLast("/", "") + val parts = blob.name.split("/") + val sb = StringBuilder() + + // Build each parent folder path + for (i in 0 until parts.size - 1) { + sb.append(parts[i]).append("/") + folders.add(sb.toString()) } } return ArrayList(folders) diff --git a/src/main/kotlin/com/epam/brn/service/impl/RoleServiceImpl.kt b/src/main/kotlin/com/epam/brn/service/impl/RoleServiceImpl.kt index b60e6d098..6f4e53f58 100644 --- a/src/main/kotlin/com/epam/brn/service/impl/RoleServiceImpl.kt +++ b/src/main/kotlin/com/epam/brn/service/impl/RoleServiceImpl.kt @@ -6,6 +6,8 @@ import com.epam.brn.exception.EntityNotFoundException import com.epam.brn.model.Role import com.epam.brn.repo.RoleRepository import com.epam.brn.service.RoleService +import org.springframework.cache.annotation.Cacheable +import org.springframework.cache.annotation.CacheEvict import org.springframework.stereotype.Service import org.springframework.web.context.request.RequestContextHolder import org.springframework.web.context.request.ServletRequestAttributes @@ -13,16 +15,19 @@ import org.springframework.web.context.request.ServletRequestAttributes @Service class RoleServiceImpl(private val roleRepository: RoleRepository) : RoleService { + @Cacheable("roles") override fun findById(id: Long): Role = roleRepository .findById(id) .orElseThrow { EntityNotFoundException("Role with id = $id is not found") } + @Cacheable("roles") override fun findByName(name: String): Role = roleRepository - .findByName(name) + .findByNameOrNull(name) ?: throw EntityNotFoundException("Role with name = $name is not found") + @CacheEvict("roles", allEntries = true) override fun save(role: Role) = roleRepository.save(role) @@ -34,6 +39,7 @@ class RoleServiceImpl(private val roleRepository: RoleRepository) : RoleService override fun isUserHasRole(user: UserAccountDto, role: String): Boolean = user.roles.contains(role) + @Cacheable("allRoles") override fun findAll(): List = roleRepository.findAll() } diff --git a/src/main/kotlin/com/epam/brn/service/impl/UserAccountServiceImpl.kt b/src/main/kotlin/com/epam/brn/service/impl/UserAccountServiceImpl.kt index 3d6e89d65..25bbe20d3 100644 --- a/src/main/kotlin/com/epam/brn/service/impl/UserAccountServiceImpl.kt +++ b/src/main/kotlin/com/epam/brn/service/impl/UserAccountServiceImpl.kt @@ -72,9 +72,11 @@ class UserAccountServiceImpl( .toSet() override fun getCurrentUser(): UserAccount { - val email = getCurrentUserEmail() - return userAccountRepository.findUserAccountByEmail(email) - .orElseThrow { EntityNotFoundException("No user was found for email=$email") } + val authentication = SecurityContextHolder.getContext().authentication + return authentication.details as? UserAccount + ?: userAccountRepository.findUserAccountByEmail(getCurrentUserEmail()) + .orElseThrow { EntityNotFoundException("No user was found for email=${authentication.name}") } + .also { authentication.details = it } } override fun markVisitForCurrentUser() { @@ -96,6 +98,7 @@ class UserAccountServiceImpl( userAccountRepository .findUsersAccountsByRole(role) .map { it.toDto() } + .toList() override fun updateAvatarForCurrentUser(avatarUrl: String): UserAccountDto { val currentUserAccount = getCurrentUser() @@ -173,8 +176,9 @@ class UserAccountServiceImpl( throw EntityNotFoundException("There is no user in the session") } - private fun getDefaultRoleSet(): MutableSet = + private val defaultRoleSet: MutableSet by lazy { setOf(BrnRole.USER).mapTo(mutableSetOf()) { roleService.findByName(it) } + } override fun deleteAutoTestUsers(): Long = userAccountRepository.deleteUserAccountsByEmailStartsWith(prefix) override fun deleteAutoTestUserByEmail(email: String): Long = diff --git a/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceImpl.kt b/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceImpl.kt index 7895a2b56..5b9a4f354 100644 --- a/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceImpl.kt +++ b/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceImpl.kt @@ -35,34 +35,36 @@ class UserAnalyticsServiceImpl( private val exerciseService: ExerciseService, ) : UserAnalyticsService { - private val listTextExercises = listOf(ExerciseType.SENTENCE, ExerciseType.PHRASES) + private val listTextExercises = setOf(ExerciseType.SENTENCE, ExerciseType.PHRASES) override fun getUsersWithAnalytics(pageable: Pageable, role: String): List { - val users = userAccountRepository.findUsersAccountsByRole(role).map { it.toAnalyticsDto() } - val now = timeService.now() - val firstWeekDay = WeekFields.of(Locale.getDefault()).dayOfWeek() - val startDay = now.with(firstWeekDay, 1L) - val from = startDay.with(LocalTime.MIN) - val to = startDay.plusDays(7L).with(LocalTime.MAX) + val (from, to) = calculateDateRange(now) val startOfCurrentMonth = now.withDayOfMonth(1).with(LocalTime.MIN) - users.onEach { user -> - user.lastWeek = userDayStatisticService.getStatisticsForPeriod(from, to, user.id) - user.studyDaysInCurrentMonth = countWorkDaysForMonth( - userDayStatisticService.getStatisticsForPeriod(startOfCurrentMonth, now, user.id) - ) + val users = userAccountRepository.findUsersAccountsByRole(role) + val userIds = users.map { it.id } + + val userStatisticsMap = studyHistoryRepository.getStatisticsByUserAccountIds(userIds) + .associateBy { it.userId } - val userStatistics = studyHistoryRepository.getStatisticsByUserAccountId(user.id) - user.apply { - this.firstDone = userStatistics.firstStudy - this.lastDone = userStatistics.lastStudy - this.spentTime = userStatistics.spentTime.toDuration(DurationUnit.SECONDS) - this.doneExercises = userStatistics.doneExercises + return users.map { user -> + val userId = user.id + val analytics = user.toAnalyticsDto().apply { + lastWeek = userDayStatisticService.getStatisticsForPeriod(from, to, userId) + studyDaysInCurrentMonth = countWorkDaysForMonth( + userDayStatisticService.getStatisticsForPeriod(startOfCurrentMonth, now, userId) + ) + + userStatisticsMap[userId]?.let { stats -> + firstDone = stats.firstStudy + lastDone = stats.lastStudy + spentTime = stats.spentTime.toDuration(DurationUnit.SECONDS) + doneExercises = stats.doneExercises + } } + analytics } - - return users } override fun prepareAudioFileForUser(exerciseId: Long, audioFileMetaData: AudioFileMetaData): InputStream = @@ -70,23 +72,23 @@ class UserAnalyticsServiceImpl( override fun prepareAudioFileMetaData(exerciseId: Long, audioFileMetaData: AudioFileMetaData): AudioFileMetaData { val currentUserId = userAccountService.getCurrentUserId() - val lastExerciseHistory = studyHistoryRepository - .findLastByUserAccountIdAndExerciseId(currentUserId, exerciseId) val seriesType = ExerciseType.valueOf(exerciseRepository.findTypeByExerciseId(exerciseId)) + + val lastExerciseHistory = if (audioFileMetaData.text.contains(" ") || !listTextExercises.contains(seriesType)) { + studyHistoryRepository.findLastByUserAccountIdAndExerciseId(currentUserId, exerciseId) + } else null - val text = audioFileMetaData.text - if (!listTextExercises.contains(seriesType)) - audioFileMetaData.text = text.replace(" ", ", ") + return audioFileMetaData.apply { + if (!listTextExercises.contains(seriesType)) { + text = text.replace(" ", ", ") + } - if (text.contains(" ")) { - if (isDoneBad(lastExerciseHistory)) - audioFileMetaData.setSpeedSlowest() - else - audioFileMetaData.setSpeedSlow() - } else if (isDoneBad(lastExerciseHistory)) { - audioFileMetaData.setSpeedSlow() + when { + text.contains(" ") && isDoneBad(lastExerciseHistory) -> setSpeedSlowest() + text.contains(" ") -> setSpeedSlow() + isDoneBad(lastExerciseHistory) -> setSpeedSlow() + } } - return audioFileMetaData } fun isDoneBad(lastHistory: StudyHistory?): Boolean = @@ -100,4 +102,12 @@ class UserAnalyticsServiceImpl( .map { it.date } .groupBy { it.dayOfMonth } .keys.size + + private fun calculateDateRange(now: LocalDateTime): Pair { + val firstWeekDay = WeekFields.of(Locale.getDefault()).dayOfWeek() + val startDay = now.with(firstWeekDay, 1L) + val from = startDay.with(LocalTime.MIN) + val to = startDay.plusDays(7L).with(LocalTime.MAX) + return from to to + } } diff --git a/src/main/kotlin/com/epam/brn/service/statistics/impl/UserDayStatisticsService.kt b/src/main/kotlin/com/epam/brn/service/statistics/impl/UserDayStatisticsService.kt index b72f5fa56..c288e84c3 100644 --- a/src/main/kotlin/com/epam/brn/service/statistics/impl/UserDayStatisticsService.kt +++ b/src/main/kotlin/com/epam/brn/service/statistics/impl/UserDayStatisticsService.kt @@ -23,15 +23,16 @@ class UserDayStatisticsService( from = from, to = to ) - return studyHistories.map { - val filteredStudyHistories = studyHistories.filter { studyHistoryFilter -> - studyHistoryFilter.startTime.toLocalDate() == it.startTime.toLocalDate() + + // Group histories by date first to avoid repeated filtering + return studyHistories + .groupBy { it.startTime.toLocalDate() } + .map { (_, dailyHistories) -> + DayStudyStatistics( + exercisingTimeSeconds = dailyHistories.sumOf { it.executionSeconds }, + date = dailyHistories.first().startTime, + progress = progressManager.getStatus(UserExercisingPeriod.DAY, dailyHistories) + ) } - DayStudyStatistics( - exercisingTimeSeconds = filteredStudyHistories.sumOf { dayStudyHistory -> dayStudyHistory.executionSeconds }, - date = it.startTime, - progress = progressManager.getStatus(UserExercisingPeriod.DAY, filteredStudyHistories) - ) - }.distinctBy { it.date.toLocalDate() } } } diff --git a/src/main/kotlin/com/epam/brn/service/statistics/impl/UserMonthStatisticsService.kt b/src/main/kotlin/com/epam/brn/service/statistics/impl/UserMonthStatisticsService.kt index 687f13122..f79f74b52 100644 --- a/src/main/kotlin/com/epam/brn/service/statistics/impl/UserMonthStatisticsService.kt +++ b/src/main/kotlin/com/epam/brn/service/statistics/impl/UserMonthStatisticsService.kt @@ -10,6 +10,7 @@ import com.epam.brn.service.statistics.progress.status.ProgressStatusManager import org.springframework.stereotype.Service import java.time.LocalDateTime import java.time.temporal.ChronoUnit +import java.time.YearMonth @Service class UserMonthStatisticsService( @@ -29,20 +30,18 @@ class UserMonthStatisticsService( from = from, to = to ) - return histories.map { - val filteredHistories = histories.filter { historyFilter -> - historyFilter.startTime.month == it.startTime.month - } + + // Group histories by month-year combination in a single pass + return histories.groupBy { + YearMonth.from(it.startTime) + }.map { (yearMonth, monthHistories) -> MonthStudyStatistics( - date = it.startTime, - exercisingTimeSeconds = filteredHistories.sumOf { studyHistory -> studyHistory.executionSeconds }, - progress = progressManager.getStatus(UserExercisingPeriod.WEEK, filteredHistories), - exercisingDays = filteredHistories.distinctBy { studyHistory -> studyHistory.startTime.truncatedTo(ChronoUnit.DAYS) }.size - ) - }.distinctBy { - listOf( - it.date.month, - it.date.year + date = monthHistories.first().startTime, + exercisingTimeSeconds = monthHistories.sumOf { it.executionSeconds }, + progress = progressManager.getStatus(UserExercisingPeriod.WEEK, monthHistories), + exercisingDays = monthHistories.distinctBy { + it.startTime.truncatedTo(ChronoUnit.DAYS) + }.size ) } } From 0be9a172e650e2b2cfd57ab17ce6f3782dafa854 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Wed, 13 Nov 2024 16:49:06 +0300 Subject: [PATCH 2/2] + --- src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt b/src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt index 4859d4d0a..2267ec4b1 100644 --- a/src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt +++ b/src/main/kotlin/com/epam/brn/repo/SeriesRepository.kt @@ -18,8 +18,11 @@ interface SeriesRepository : CrudRepository { fun findByNameIn(names: List): List - @Query("select distinct s from Series s left join fetch s.exerciseGroup where s.exerciseGroup.id = :groupId") - fun findByExerciseGroupLike(groupId: Long): List + @Query(""" + select s from Series s + where s.exerciseGroup.id = :groupId + """) + fun findByExerciseGroupId(groupId: Long): List @Query("select distinct s from Series s left join fetch s.exercises where s.id = :seriesId") fun findSeriesWithExercisesById(seriesId: Long): Optional