diff --git a/.gitignore b/.gitignore index 332b36a..7890366 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ build/ ### Environment Variables ### src/main/resources/.env.properties + +### DB Data ### +db/redis/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0976592..16784ec 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,12 @@ spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-data-redis + + com.h2database @@ -129,6 +135,20 @@ + + + + + + + + + + + + + + diff --git a/src/main/java/com/seyed/ali/timeentryservice/config/cache/RedisConfiguration.java b/src/main/java/com/seyed/ali/timeentryservice/config/cache/RedisConfiguration.java new file mode 100644 index 0000000..505e37b --- /dev/null +++ b/src/main/java/com/seyed/ali/timeentryservice/config/cache/RedisConfiguration.java @@ -0,0 +1,52 @@ +package com.seyed.ali.timeentryservice.config.cache; + +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericToStringSerializer; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; + +@Configuration +@EnableCaching +public class RedisConfiguration { + + /** + * Remember to configure the properties in application.yml or properties file. + */ + @Bean + @Primary + public RedisProperties properties() { + return new RedisProperties(); + } + + /** + * Lettuce is non-blocking, and Jedis is blocking. + */ + @Bean + public LettuceConnectionFactory lettuceConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(this.properties().getHost()); + redisStandaloneConfiguration.setPort(this.properties().getPort()); + + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + + redisTemplate.setConnectionFactory(this.lettuceConnectionFactory()); + redisTemplate.setEnableTransactionSupport(true); + + redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer()); + redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Object.class)); + + return redisTemplate; + } + +} diff --git a/src/main/java/com/seyed/ali/timeentryservice/controller/TimeEntryController.java b/src/main/java/com/seyed/ali/timeentryservice/controller/TimeEntryController.java index 1af36eb..963d5e8 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/controller/TimeEntryController.java +++ b/src/main/java/com/seyed/ali/timeentryservice/controller/TimeEntryController.java @@ -42,7 +42,7 @@ public ResponseEntity getTimeEntries() { )); } - @GetMapping("/{userId}") + @GetMapping("/user/{userId}") public ResponseEntity getUsersTimeEntry(@PathVariable String userId) { return ResponseEntity.ok(new Result( true, @@ -52,6 +52,16 @@ public ResponseEntity getUsersTimeEntry(@PathVariable String userId) { )); } + @GetMapping("/{timeEntryId}") + public ResponseEntity getSpecificTimeEntry(@PathVariable String timeEntryId) { + return ResponseEntity.ok(new Result( + true, + OK, + "Time entry: '" + timeEntryId + "'.", + this.timeEntryService.getTimeEntryById(timeEntryId) + )); + } + @PostMapping public ResponseEntity addTimeEntryManually(@Valid @RequestBody TimeEntryDTO timeEntryDTO) { return ResponseEntity.status(CREATED).body(new Result( diff --git a/src/main/java/com/seyed/ali/timeentryservice/controller/TimeEntryTrackingController.java b/src/main/java/com/seyed/ali/timeentryservice/controller/TimeEntryTrackingController.java index a3049d1..c46ed10 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/controller/TimeEntryTrackingController.java +++ b/src/main/java/com/seyed/ali/timeentryservice/controller/TimeEntryTrackingController.java @@ -24,18 +24,11 @@ public class TimeEntryTrackingController { @PostMapping("/start") public ResponseEntity startTrackingTimeEntry(@Valid @RequestBody TimeBillingDTO timeBillingDTO) { - boolean billable = false; - BigDecimal hourlyRate = BigDecimal.ZERO; - - if (timeBillingDTO != null) { - billable = timeBillingDTO.billable(); - hourlyRate = timeBillingDTO.hourlyRate(); - } return ResponseEntity.status(CREATED).body(new Result( true, CREATED, "Time tracking started...", - this.timeEntryService.startTrackingTimeEntry(billable, hourlyRate) + this.timeEntryService.startTrackingTimeEntry(timeBillingDTO) )); } diff --git a/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryServiceImpl.java b/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryServiceImpl.java index 2a71ca7..57908cb 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryServiceImpl.java +++ b/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryServiceImpl.java @@ -11,6 +11,8 @@ import com.seyed.ali.timeentryservice.util.TimeParser; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -44,6 +46,20 @@ public TimeEntryResponse getUsersTimeEntry(String userId) { return this.timeEntryUtility.convertToTimeEntryResponse(timeEntry); } + /** + * {@inheritDoc} + */ + @Override +// @Cacheable( +// cacheNames = "time-entry-cache", +// key = "#timeEntryId" +// ) + public TimeEntryResponse getTimeEntryById(String timeEntryId) { + return this.timeEntryRepository.findById(timeEntryId) + .map(this.timeEntryUtility::convertToTimeEntryResponse) + .orElseThrow(()-> new ResourceNotFoundException("Time entry with ID: '" + timeEntryId +"' was not found.")); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryTrackingServiceImpl.java b/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryTrackingServiceImpl.java index aff0c4e..c5cafb1 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryTrackingServiceImpl.java +++ b/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryTrackingServiceImpl.java @@ -3,6 +3,7 @@ import com.seyed.ali.timeentryservice.client.AuthenticationServiceClient; import com.seyed.ali.timeentryservice.exceptions.OperationNotSupportedException; import com.seyed.ali.timeentryservice.model.domain.TimeEntry; +import com.seyed.ali.timeentryservice.model.dto.TimeBillingDTO; import com.seyed.ali.timeentryservice.model.dto.TimeEntryDTO; import com.seyed.ali.timeentryservice.repository.TimeEntryRepository; import com.seyed.ali.timeentryservice.service.interfaces.TimeEntryTrackingService; @@ -10,6 +11,8 @@ import com.seyed.ali.timeentryservice.util.TimeParser; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,7 +36,19 @@ public class TimeEntryTrackingServiceImpl implements TimeEntryTrackingService { */ @Override @Transactional - public String startTrackingTimeEntry(boolean billable, BigDecimal hourlyRate) { + @CachePut( + cacheNames = "time-entry-cache", + key = "#result" + ) + public String startTrackingTimeEntry(TimeBillingDTO timeBillingDTO) { + boolean billable = false; + BigDecimal hourlyRate = BigDecimal.ZERO; + + if (timeBillingDTO != null) { + billable = timeBillingDTO.billable(); + hourlyRate = timeBillingDTO.hourlyRate(); + } + TimeEntry timeEntry = this.timeEntryUtility.createNewTimeEntry(billable, hourlyRate, this.authenticationServiceClient); this.timeEntryRepository.save(timeEntry); return timeEntry.getTimeEntryId(); diff --git a/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryService.java b/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryService.java index 13cfd9e..ff3de86 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryService.java +++ b/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryService.java @@ -27,6 +27,15 @@ public interface TimeEntryService { */ TimeEntryResponse getUsersTimeEntry(String userId); + /** + * Retrieves a specific time entry. + * + * @param timeEntryId The ID of the time entry. + * @return A TimeEntryResponse object representing the found time entry. + * @throws ResourceNotFoundException if the time entry is not found. + */ + TimeEntryResponse getTimeEntryById(String timeEntryId); + /** * Adds a new time entry manually. * diff --git a/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryTrackingService.java b/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryTrackingService.java index 221b455..b2fcac6 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryTrackingService.java +++ b/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryTrackingService.java @@ -1,9 +1,8 @@ package com.seyed.ali.timeentryservice.service.interfaces; +import com.seyed.ali.timeentryservice.model.dto.TimeBillingDTO; import com.seyed.ali.timeentryservice.model.dto.TimeEntryDTO; -import java.math.BigDecimal; - /** * Interface for Time Entry tracking service operations. */ @@ -12,11 +11,10 @@ public interface TimeEntryTrackingService { /** * Starts tracking a new time entry. * - * @param billable A boolean indicating whether the time entry is billable or not. - * @param hourlyRate The hourly rate for the time entry (if billable). + * @param timeBillingDTO A dto class with: A boolean indicating whether the time entry is billable or not & The hourly rate for the time entry (if billable). * @return The ID of the created time entry. */ - String startTrackingTimeEntry(boolean billable, BigDecimal hourlyRate); + String startTrackingTimeEntry(TimeBillingDTO timeBillingDTO); /** * Stops tracking an existing time entry. diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 532c102..fc151f9 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -44,6 +44,20 @@ spring: jwt: issuer-uri: http://${KEYCLOAK_SERVER_HOST:localhost}:8080/realms/DevVault-v2.0 +--- # Redis +spring: + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + cache: + type: redis + cache-names: + - time-entry-cache + redis: + cache-null-values: true + time-to-live: ${REDIS_CACHE_TIME_TO_LIVE:600000} # 1 hour + --- # Swagger springdoc: swagger-ui: diff --git a/src/test/java/com/seyed/ali/timeentryservice/controller/TimeEntryControllerTest.java b/src/test/java/com/seyed/ali/timeentryservice/controller/TimeEntryControllerTest.java index d8afed8..f881465 100644 --- a/src/test/java/com/seyed/ali/timeentryservice/controller/TimeEntryControllerTest.java +++ b/src/test/java/com/seyed/ali/timeentryservice/controller/TimeEntryControllerTest.java @@ -135,7 +135,7 @@ public void getUsersTimeEntryTest() throws Exception { // When ResultActions resultActions = this.mockMvc.perform( - MockMvcRequestBuilders.get(this.baseUrl + "/" + userId) + MockMvcRequestBuilders.get(this.baseUrl + "/user/" + userId) .accept(APPLICATION_JSON) .with(jwt().authorities(new SimpleGrantedAuthority(some_authority))) ); diff --git a/src/test/java/com/seyed/ali/timeentryservice/controller/TimeEntryTrackingControllerTest.java b/src/test/java/com/seyed/ali/timeentryservice/controller/TimeEntryTrackingControllerTest.java index 392f65f..96cae16 100644 --- a/src/test/java/com/seyed/ali/timeentryservice/controller/TimeEntryTrackingControllerTest.java +++ b/src/test/java/com/seyed/ali/timeentryservice/controller/TimeEntryTrackingControllerTest.java @@ -60,7 +60,7 @@ void setUp() { public void startTrackingTimeEntryTest() throws Exception { // given String timeEntryId = "some_time_entry_id"; - when(this.timeEntryTrackingService.startTrackingTimeEntry(isA(Boolean.class), isA(BigDecimal.class))) + when(this.timeEntryTrackingService.startTrackingTimeEntry(isA(TimeBillingDTO.class))) .thenReturn(timeEntryId); String json = this.objectMapper.writeValueAsString(new TimeBillingDTO(true, BigDecimal.ONE)); diff --git a/src/test/java/com/seyed/ali/timeentryservice/service/TimeEntryTrackingServiceImplTest.java b/src/test/java/com/seyed/ali/timeentryservice/service/TimeEntryTrackingServiceImplTest.java index 2cc1a93..835d84a 100644 --- a/src/test/java/com/seyed/ali/timeentryservice/service/TimeEntryTrackingServiceImplTest.java +++ b/src/test/java/com/seyed/ali/timeentryservice/service/TimeEntryTrackingServiceImplTest.java @@ -3,6 +3,7 @@ import com.seyed.ali.timeentryservice.client.AuthenticationServiceClient; import com.seyed.ali.timeentryservice.model.domain.TimeEntry; import com.seyed.ali.timeentryservice.model.domain.TimeSegment; +import com.seyed.ali.timeentryservice.model.dto.TimeBillingDTO; import com.seyed.ali.timeentryservice.model.dto.TimeEntryDTO; import com.seyed.ali.timeentryservice.repository.TimeEntryRepository; import com.seyed.ali.timeentryservice.util.TimeEntryUtility; @@ -84,7 +85,8 @@ public void startTrackingTimeEntryTest() { when(this.timeEntryRepository.save(isA(TimeEntry.class))).thenReturn(timeEntry); // Act - String timeEntryId = this.timeEntryTrackingService.startTrackingTimeEntry(false, BigDecimal.ONE); + TimeBillingDTO timeBillingDTO = new TimeBillingDTO(false, BigDecimal.ZERO); + String timeEntryId = this.timeEntryTrackingService.startTrackingTimeEntry(timeBillingDTO); System.out.println(timeEntryId); // Assert